├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── demo ├── background-less.html ├── homepage.html ├── images │ ├── brush.png │ ├── brush.svg │ ├── default-blurred.jpg │ ├── default.jpg │ ├── github.svg │ ├── homepage-blurred.jpg │ ├── homepage.jpg │ ├── keep-cleared-blurred.jpg │ ├── keep-cleared.jpg │ ├── line-style-blurred.jpg │ ├── line-style.jpg │ └── separator.png ├── keep-cleared.html ├── line-style.html ├── partials │ ├── examples.html │ ├── footer.html │ └── header.html ├── scripts │ └── demo.js ├── sticky.html └── styles │ ├── common.scss │ └── demo.scss ├── dist ├── brusher.min.js ├── brusher.min.js.map └── demo │ ├── background-less.html │ ├── brusher-demo.min.css │ ├── brusher-demo.min.js │ ├── demo.html │ ├── images │ ├── brush.png │ ├── brush.svg │ ├── default-blurred.jpg │ ├── default.jpg │ ├── github.svg │ ├── homepage-blurred.jpg │ ├── homepage.jpg │ ├── keep-cleared-blurred.jpg │ ├── keep-cleared.jpg │ ├── line-style-blurred.jpg │ ├── line-style.jpg │ ├── non-sticky-blurred.jpg │ ├── non-sticky.jpg │ └── separator.png │ ├── index.html │ ├── keep-cleared.html │ ├── line-style.html │ ├── non-sticky.html │ └── sticky.html ├── license ├── package.json ├── readme.md ├── server.js ├── src └── index.js ├── webpack.config.demo.js ├── webpack.config.prod.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "no-console": "off", 8 | "no-underscore-dangle": "off", 9 | "no-plusplus": "off", 10 | "no-cond-assign": "off", 11 | "func-names": "off", 12 | "no-continue": "off", 13 | "no-bitwise": "off", 14 | "class-methods-use-this": "off", 15 | "prefer-destructuring": "off", 16 | "no-restricted-syntax": "off", 17 | "no-param-reassign": [ 18 | "off" 19 | ], 20 | "max-len": "off", 21 | "no-multi-spaces": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | npm-debug.log 4 | .DS_Store 5 | .vscode 6 | package-lock.json 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /demo/background-less.html: -------------------------------------------------------------------------------- 1 | <%= require('html-loader!./partials/header.html') %> 2 | 3 |
4 |
5 |
6 |
7 | 8 |
9 |

No Background

10 |

If you want to use it without the blurry background, all you have to do is not set the background on body and use it normally

11 |
12 | 13 |
const brusher = new Brusher({
14 |     image: 'path/to/image.png'
15 | });
16 | 
17 | brusher.init();
18 | 
19 | 20 | ← Back to Home 21 |
22 | <%= require('html-loader!./partials/examples.html') %> 23 |
24 | 25 | <%= require('html-loader!./partials/footer.html') %> 26 | -------------------------------------------------------------------------------- /demo/homepage.html: -------------------------------------------------------------------------------- 1 | <%= require('html-loader!./partials/header.html') %> 2 | 3 |
4 |
5 |
6 |
7 | 8 |
9 |

brusher

10 |

A lightweight library to create interactive backgrounds

11 | 12 | Star 13 |
14 |
15 | 16 |
17 |

Background of this page has been auto created by brusher. Move your mouse around to see it in action.

18 |

All you need is an image. Below code shows how the current page has been initialized

19 |
const brusher = new Brusher({
20 |   image: 'abstract.png'
21 | });
22 | 
23 | brusher.init();
24 | 
25 | 26 |

There are several options that you can use to modify the result. Have a look at the section below.

27 |
28 | 29 |
30 |

Available Options

31 |

Here is the list of options that you may use

32 |
const brusher = new Brusher({
33 |   image: 'abstract.png', // Path of the image to be used as a brush
34 |   keepCleared: false, // Whether to keep cleared sections or blur them again
35 |   stroke: 80, // Stroke size for the brush
36 |   lineStyle: 'round', // Brush style (round, square, butt)
37 |   autoBlur: false, // Brusher will use the provided image for the blurry background
38 |   autoBlurValue: 15, // Blur strength in pixels
39 | });
40 | 
41 | brusher.init();
42 | 
43 | 44 |

Have a look at the github page for further details.

45 |
46 | 47 | <%= require('html-loader!./partials/examples.html') %> 48 |
49 | 50 | <%= require('html-loader!./partials/footer.html') %> 51 | -------------------------------------------------------------------------------- /demo/images/brush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/brush.png -------------------------------------------------------------------------------- /demo/images/brush.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 72 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /demo/images/default-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/default-blurred.jpg -------------------------------------------------------------------------------- /demo/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/default.jpg -------------------------------------------------------------------------------- /demo/images/github.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/github.svg -------------------------------------------------------------------------------- /demo/images/homepage-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/homepage-blurred.jpg -------------------------------------------------------------------------------- /demo/images/homepage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/homepage.jpg -------------------------------------------------------------------------------- /demo/images/keep-cleared-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/keep-cleared-blurred.jpg -------------------------------------------------------------------------------- /demo/images/keep-cleared.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/keep-cleared.jpg -------------------------------------------------------------------------------- /demo/images/line-style-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/line-style-blurred.jpg -------------------------------------------------------------------------------- /demo/images/line-style.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/line-style.jpg -------------------------------------------------------------------------------- /demo/images/separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/demo/images/separator.png -------------------------------------------------------------------------------- /demo/keep-cleared.html: -------------------------------------------------------------------------------- 1 | <%= require('html-loader!./partials/header.html') %> 2 | 3 |
4 |
5 |
6 |
7 | 8 |
9 |

Preserve Cleared

10 |

If you want brusher to preserve the cleared sections, set keepCleared to true

11 |
12 | 13 |
const brusher = new Brusher({
14 |   image: 'path/to/image.png',
15 |   keepCleared: true,
16 | });
17 | 
18 | brusher.init();
19 | 
20 | 21 | ← Back to Home 22 |
23 | <%= require('html-loader!./partials/examples.html') %> 24 |
25 | 26 | <%= require('html-loader!./partials/footer.html') %> 27 | -------------------------------------------------------------------------------- /demo/line-style.html: -------------------------------------------------------------------------------- 1 | <%= require('html-loader!./partials/header.html') %> 2 | 3 |
4 |
5 |
6 |
7 | 8 |
9 |

Line Styles

10 |

There are three different line styles that brusher understands i.e. round, square, butt. If you don't provide it, it will use round by default. Also, depending upon the image, the effect might not be quite visible in the background.

11 |
12 | 13 |
const brusher = new Brusher({
14 |   image: 'path/to/image.png',
15 |   lineStyle: 'butt',
16 | });
17 | 
18 | brusher.init();
19 | 
20 | ← Back to Home 21 |
22 | <%= require('html-loader!./partials/examples.html') %> 23 |
24 | 25 | <%= require('html-loader!./partials/footer.html') %> 26 | -------------------------------------------------------------------------------- /demo/partials/examples.html: -------------------------------------------------------------------------------- 1 |
2 |

Examples

3 |

Find some of the examples demonstrating some of the options in use through the buttons below.

4 | 5 | Keep Cleared Sections 6 | Without Background Demo 7 | Sticky Blur Demo 8 | Different Line Style 9 | 10 | Github 11 | 12 | 13 |
-------------------------------------------------------------------------------- /demo/partials/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/partials/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Brusher - Create Interactive Backgrounds for Webpages 8 | 9 | 10 | 11 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /demo/scripts/demo.js: -------------------------------------------------------------------------------- 1 | import Brusher from '../../src'; 2 | 3 | const classMapping = { 4 | homepage: { 5 | image: 'images/homepage.jpg', 6 | }, 7 | 'sticky-blur': { 8 | image: 'images/default.jpg', 9 | }, 10 | 'keep-cleared': { 11 | image: 'images/keep-cleared.jpg', 12 | stroke: 50, 13 | keepCleared: true, 14 | }, 15 | 'line-style': { 16 | image: 'images/line-style.jpg', 17 | stroke: 70, 18 | lineStyle: 'butt', 19 | }, 20 | }; 21 | 22 | function initHighlighter() { 23 | const codeElements = document.querySelectorAll('pre code'); 24 | for (let counter = 0; counter < codeElements.length; counter++) { 25 | hljs.highlightBlock(codeElements[counter]); // eslint-disable-line 26 | } 27 | } 28 | 29 | function initBrusher() { 30 | const bodyClasses = document.body.classList; 31 | let options = classMapping.homepage; 32 | 33 | for (const currentClass in classMapping) { 34 | if (bodyClasses.contains(currentClass)) { 35 | options = classMapping[currentClass]; 36 | } 37 | } 38 | 39 | const brusher = new Brusher(options); 40 | brusher.init(); 41 | } 42 | 43 | initBrusher(); 44 | initHighlighter(); 45 | -------------------------------------------------------------------------------- /demo/sticky.html: -------------------------------------------------------------------------------- 1 | <%= require('html-loader!./partials/header.html') %> 2 | 3 |
4 |
5 |
6 |
7 | 8 |
9 |

Sticky Blur

10 |

If you want the cleared sections to be not preserved, all you have to do is set keepCleared to false; or not set it since it defaults to false

11 |
12 | 13 |
const brusher = new Brusher({
14 |   image: 'path/to/image.png',
15 |   keepCleared: false,
16 | });
17 | 
18 | brusher.init();
19 | 
20 | ← Back to Home 21 |
22 | <%= require('html-loader!./partials/examples.html') %> 23 |
24 | 25 | <%= require('html-loader!./partials/footer.html') %> 26 | -------------------------------------------------------------------------------- /demo/styles/common.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | *, ::after, ::before { 7 | box-sizing: border-box; 8 | } 9 | 10 | html, body { 11 | position: relative; 12 | overflow-x: hidden; 13 | display: block; 14 | width: 100%; 15 | height: 100%; 16 | line-height: 1.5; 17 | } 18 | 19 | body { 20 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 21 | text-align: left; 22 | background-color: #fff; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | margin-bottom: .5rem; 27 | font-family: inherit; 28 | line-height: 1.2; 29 | } 30 | 31 | h1 { 32 | font-size: 2.5rem; 33 | } 34 | 35 | h2 { 36 | font-size: 2rem; 37 | } 38 | 39 | h3 { 40 | font-size: 1.5rem; 41 | } 42 | 43 | p { 44 | margin-top: 0; 45 | margin-bottom: 1rem; 46 | } 47 | 48 | code { 49 | border-radius: 4px; 50 | font-size: 1rem; 51 | margin-bottom: 15px; 52 | } 53 | 54 | ul li { 55 | list-style-type: none; 56 | } 57 | 58 | pre { 59 | text-align: left; 60 | } 61 | 62 | hr { 63 | height: 4px; 64 | background-size: cover; 65 | margin-bottom: 15px; 66 | background: url(images/separator.png) repeat-y center; 67 | border: none; 68 | } 69 | 70 | .header-wrap { 71 | max-width: 350px; 72 | margin: auto; 73 | } 74 | 75 | .bg-highlight { 76 | background: #ffc !important; 77 | } 78 | 79 | .panels-wrap { 80 | margin-bottom: 150px; 81 | } 82 | 83 | .panel { 84 | background: white; 85 | max-width: 450px; 86 | border-radius: 10px; 87 | margin: 10px auto; 88 | text-align: left; 89 | padding: 20px; 90 | 91 | p:last-child { 92 | margin-bottom: 0; 93 | } 94 | 95 | &.panel-first { 96 | text-align: center; 97 | margin-top: 225px; 98 | 99 | p { 100 | margin-bottom: 20px; 101 | } 102 | } 103 | 104 | &.panel-social { 105 | a { 106 | color: white; 107 | margin-left: 10px; 108 | text-decoration: none; 109 | } 110 | 111 | text-align: center; 112 | padding: 10px; 113 | background: transparent; 114 | margin-top: 0; 115 | } 116 | 117 | .img-wrap { 118 | max-width: 155px; 119 | margin: -100px auto 0; 120 | background: white; 121 | border-radius: 100%; 122 | padding: 20px; 123 | 124 | img { 125 | width: 100%; 126 | } 127 | } 128 | } 129 | 130 | code.dangling { 131 | background: whitesmoke; 132 | padding: 5px; 133 | font-size: 15px; 134 | color: #ef4823; 135 | } 136 | 137 | .btn { 138 | text-align: center; 139 | display: block; 140 | margin-bottom: 3px; 141 | text-decoration: none; 142 | padding: 10px; 143 | border-radius: 5px; 144 | color: white; 145 | background: #5e8e9c; 146 | } 147 | 148 | .btn-back { 149 | margin-top: -10px; 150 | } 151 | 152 | .btn-github { 153 | background: #212121; 154 | 155 | svg { 156 | fill: white; 157 | margin-left: 2px; 158 | height: 18px; 159 | position: relative; 160 | top: 3px; 161 | } 162 | } 163 | 164 | .btn-blue { 165 | background: #2020ff; 166 | } 167 | 168 | .btn-red { 169 | background: #ef4823; 170 | } 171 | 172 | .btn-run { 173 | margin-top: -10px; 174 | } 175 | 176 | .non-btn { 177 | margin-top: 15px; 178 | display: block; 179 | color: #3700ff; 180 | text-decoration: none; 181 | } -------------------------------------------------------------------------------- /demo/styles/demo.scss: -------------------------------------------------------------------------------- 1 | @import "common"; 2 | 3 | body { 4 | background-size: cover; 5 | background-position: 0 0; 6 | background-attachment: fixed; 7 | 8 | &.homepage { 9 | background-image: url('images/homepage-blurred.jpg'); 10 | } 11 | 12 | &.sticky-blur { 13 | background-image: url('images/default-blurred.jpg'); 14 | } 15 | 16 | &.keep-cleared { 17 | background-image: url('images/keep-cleared-blurred.jpg'); 18 | } 19 | 20 | &.line-style { 21 | background-image: url('images/line-style-blurred.jpg'); 22 | } 23 | 24 | &.background-less { 25 | background: #5a5a5a; 26 | } 27 | } -------------------------------------------------------------------------------- /dist/brusher.min.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.Brusher=n():t.Brusher=n()}(window,function(){return function(t){var n={};function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=t,e.c=n,e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:r})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,n){if(1&n&&(t=e(t)),8&n)return t;if(4&n&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var o in t)e.d(r,o,function(n){return t[n]}.bind(null,o));return r},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},e.p="",e(e.s=44)}([function(t,n,e){var r=e(15)("wks"),o=e(11),i=e(1).Symbol,a="function"==typeof i;(t.exports=function(t){return r[t]||(r[t]=a&&i[t]||(a?i:o)("Symbol."+t))}).store=r},function(t,n){var e=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=e)},function(t,n,e){var r=e(1),o=e(10),i=e(6),a=e(12),u=e(30),s=function(t,n,e){var c,f,l,p,h=t&s.F,v=t&s.G,y=t&s.S,d=t&s.P,m=t&s.B,g=v?r:y?r[n]||(r[n]={}):(r[n]||{}).prototype,b=v?o:o[n]||(o[n]={}),S=b.prototype||(b.prototype={});for(c in v&&(e=n),e)l=((f=!h&&g&&void 0!==g[c])?g:e)[c],p=m&&f?u(l,r):d&&"function"==typeof l?u(Function.call,l):l,g&&a(g,c,l,t&s.U),b[c]!=l&&i(b,c,p),d&&S[c]!=l&&(S[c]=l)};r.core=o,s.F=1,s.G=2,s.S=4,s.P=8,s.B=16,s.W=32,s.U=64,s.R=128,t.exports=s},function(t,n,e){var r=e(16),o=e(28),i=e(19),a=Object.defineProperty;n.f=e(5)?Object.defineProperty:function(t,n,e){if(r(t),n=i(n,!0),r(e),o)try{return a(t,n,e)}catch(t){}if("get"in e||"set"in e)throw TypeError("Accessors not supported!");return"value"in e&&(t[n]=e.value),t}},function(t,n){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,n,e){t.exports=!e(8)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,n,e){var r=e(3),o=e(17);t.exports=e(5)?function(t,n,e){return r.f(t,n,o(1,e))}:function(t,n,e){return t[n]=e,t}},function(t,n){var e={}.hasOwnProperty;t.exports=function(t,n){return e.call(t,n)}},function(t,n){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,n,e){var r=e(33),o=e(34);t.exports=function(t){return r(o(t))}},function(t,n){var e=t.exports={version:"2.6.9"};"number"==typeof __e&&(__e=e)},function(t,n){var e=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++e+r).toString(36))}},function(t,n,e){var r=e(1),o=e(6),i=e(7),a=e(11)("src"),u=e(48),s=(""+u).split("toString");e(10).inspectSource=function(t){return u.call(t)},(t.exports=function(t,n,e,u){var c="function"==typeof e;c&&(i(e,"name")||o(e,"name",n)),t[n]!==e&&(c&&(i(e,a)||o(e,a,t[n]?""+t[n]:s.join(String(n)))),t===r?t[n]=e:u?t[n]?t[n]=e:o(t,n,e):(delete t[n],o(t,n,e)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[a]||u.call(this)})},function(t,n,e){var r=e(39),o=e(23);t.exports=Object.keys||function(t){return r(t,o)}},function(t,n){t.exports=!1},function(t,n,e){var r=e(10),o=e(1),i=o["__core-js_shared__"]||(o["__core-js_shared__"]={});(t.exports=function(t,n){return i[t]||(i[t]=void 0!==n?n:{})})("versions",[]).push({version:r.version,mode:e(14)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(t,n,e){var r=e(4);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},function(t,n){t.exports=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}}},function(t,n,e){var r=e(34);t.exports=function(t){return Object(r(t))}},function(t,n,e){var r=e(4);t.exports=function(t,n){if(!r(t))return t;var e,o;if(n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;if("function"==typeof(e=t.valueOf)&&!r(o=e.call(t)))return o;if(!n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,n){var e={}.toString;t.exports=function(t){return e.call(t).slice(8,-1)}},function(t,n,e){var r=e(3).f,o=e(7),i=e(0)("toStringTag");t.exports=function(t,n,e){t&&!o(t=e?t:t.prototype,i)&&r(t,i,{configurable:!0,value:n})}},function(t,n,e){var r=e(15)("keys"),o=e(11);t.exports=function(t){return r[t]||(r[t]=o(t))}},function(t,n){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,n){n.f={}.propertyIsEnumerable},function(t,n){t.exports={}},function(t,n,e){var r=e(1),o=e(10),i=e(14),a=e(27),u=e(3).f;t.exports=function(t){var n=o.Symbol||(o.Symbol=i?{}:r.Symbol||{});"_"==t.charAt(0)||t in n||u(n,t,{value:a.f(t)})}},function(t,n,e){n.f=e(0)},function(t,n,e){t.exports=!e(5)&&!e(8)(function(){return 7!=Object.defineProperty(e(29)("div"),"a",{get:function(){return 7}}).a})},function(t,n,e){var r=e(4),o=e(1).document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},function(t,n,e){var r=e(31);t.exports=function(t,n,e){if(r(t),void 0===n)return t;switch(e){case 1:return function(e){return t.call(n,e)};case 2:return function(e,r){return t.call(n,e,r)};case 3:return function(e,r,o){return t.call(n,e,r,o)}}return function(){return t.apply(n,arguments)}}},function(t,n){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,n,e){var r=e(30),o=e(33),i=e(18),a=e(35),u=e(49);t.exports=function(t,n){var e=1==t,s=2==t,c=3==t,f=4==t,l=6==t,p=5==t||l,h=n||u;return function(n,u,v){for(var y,d,m=i(n),g=o(m),b=r(u,v,3),S=a(g.length),x=0,w=e?h(n,S):s?h(n,0):void 0;S>x;x++)if((p||x in g)&&(d=b(y=g[x],x,m),t))if(e)w[x]=d;else if(d)switch(t){case 3:return!0;case 5:return y;case 6:return x;case 2:w.push(y)}else if(f)return!1;return l?-1:c||f?f:w}}},function(t,n,e){var r=e(20);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},function(t,n){t.exports=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t}},function(t,n,e){var r=e(36),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},function(t,n){var e=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:e)(t)}},function(t,n,e){var r=e(20);t.exports=Array.isArray||function(t){return"Array"==r(t)}},function(t,n,e){"use strict";var r=e(8);t.exports=function(t,n){return!!t&&r(function(){n?t.call(null,function(){},1):t.call(null)})}},function(t,n,e){var r=e(7),o=e(9),i=e(55)(!1),a=e(22)("IE_PROTO");t.exports=function(t,n){var e,u=o(t),s=0,c=[];for(e in u)e!=a&&r(u,e)&&c.push(e);for(;n.length>s;)r(u,e=n[s++])&&(~i(c,e)||c.push(e));return c}},function(t,n){n.f=Object.getOwnPropertySymbols},function(t,n,e){var r=e(16),o=e(57),i=e(23),a=e(22)("IE_PROTO"),u=function(){},s=function(){var t,n=e(29)("iframe"),r=i.length;for(n.style.display="none",e(58).appendChild(n),n.src="javascript:",(t=n.contentWindow.document).open(),t.write(" 11 | 12 | 13 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |

No Background

30 |

If you want to use it without the blurry background, all you have to do is not set the background on body and use it normally

31 |
32 | 33 |
const brusher = new Brusher({
34 |     image: 'path/to/image.png'
35 | });
36 | 
37 | brusher.init();
38 | 
39 | 40 | ← Back to Home 41 |
42 |
43 |

Examples

44 |

Find some of the examples demonstrating some of the options in use through the buttons below.

45 | 46 | Keep Cleared Sections 47 | Without Background Demo 48 | Sticky Blur Demo 49 | Different Line Style 50 | 51 | Github 52 | 53 | 54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /dist/demo/brusher-demo.min.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; } 4 | 5 | *, ::after, ::before { 6 | box-sizing: border-box; } 7 | 8 | html, body { 9 | position: relative; 10 | overflow-x: hidden; 11 | display: block; 12 | width: 100%; 13 | height: 100%; 14 | line-height: 1.5; } 15 | 16 | body { 17 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 18 | text-align: left; 19 | background-color: #fff; } 20 | 21 | h1, h2, h3, h4, h5, h6 { 22 | margin-bottom: .5rem; 23 | font-family: inherit; 24 | line-height: 1.2; } 25 | 26 | h1 { 27 | font-size: 2.5rem; } 28 | 29 | h2 { 30 | font-size: 2rem; } 31 | 32 | h3 { 33 | font-size: 1.5rem; } 34 | 35 | p { 36 | margin-top: 0; 37 | margin-bottom: 1rem; } 38 | 39 | code { 40 | border-radius: 4px; 41 | font-size: 1rem; 42 | margin-bottom: 15px; } 43 | 44 | ul li { 45 | list-style-type: none; } 46 | 47 | pre { 48 | text-align: left; } 49 | 50 | hr { 51 | height: 4px; 52 | background-size: cover; 53 | margin-bottom: 15px; 54 | background: url(images/separator.png) repeat-y center; 55 | border: none; } 56 | 57 | .header-wrap { 58 | max-width: 350px; 59 | margin: auto; } 60 | 61 | .bg-highlight { 62 | background: #ffc !important; } 63 | 64 | .panels-wrap { 65 | margin-bottom: 150px; } 66 | 67 | .panel { 68 | background: white; 69 | max-width: 450px; 70 | border-radius: 10px; 71 | margin: 10px auto; 72 | text-align: left; 73 | padding: 20px; } 74 | .panel p:last-child { 75 | margin-bottom: 0; } 76 | .panel.panel-first { 77 | text-align: center; 78 | margin-top: 225px; } 79 | .panel.panel-first p { 80 | margin-bottom: 20px; } 81 | .panel.panel-social { 82 | text-align: center; 83 | padding: 10px; 84 | background: transparent; 85 | margin-top: 0; } 86 | .panel.panel-social a { 87 | color: white; 88 | margin-left: 10px; 89 | text-decoration: none; } 90 | .panel .img-wrap { 91 | max-width: 155px; 92 | margin: -100px auto 0; 93 | background: white; 94 | border-radius: 100%; 95 | padding: 20px; } 96 | .panel .img-wrap img { 97 | width: 100%; } 98 | 99 | code.dangling { 100 | background: whitesmoke; 101 | padding: 5px; 102 | font-size: 15px; 103 | color: #ef4823; } 104 | 105 | .btn { 106 | text-align: center; 107 | display: block; 108 | margin-bottom: 3px; 109 | text-decoration: none; 110 | padding: 10px; 111 | border-radius: 5px; 112 | color: white; 113 | background: #5e8e9c; } 114 | 115 | .btn-back { 116 | margin-top: -10px; } 117 | 118 | .btn-github { 119 | background: #212121; } 120 | .btn-github svg { 121 | fill: white; 122 | margin-left: 2px; 123 | height: 18px; 124 | position: relative; 125 | top: 3px; } 126 | 127 | .btn-blue { 128 | background: #2020ff; } 129 | 130 | .btn-red { 131 | background: #ef4823; } 132 | 133 | .btn-run { 134 | margin-top: -10px; } 135 | 136 | .non-btn { 137 | margin-top: 15px; 138 | display: block; 139 | color: #3700ff; 140 | text-decoration: none; } 141 | 142 | body { 143 | background-size: cover; 144 | background-position: 0 0; 145 | background-attachment: fixed; } 146 | body.homepage { 147 | background-image: url("images/homepage-blurred.jpg"); } 148 | body.sticky-blur { 149 | background-image: url("images/default-blurred.jpg"); } 150 | body.keep-cleared { 151 | background-image: url("images/keep-cleared-blurred.jpg"); } 152 | body.line-style { 153 | background-image: url("images/line-style-blurred.jpg"); } 154 | body.background-less { 155 | background: #5a5a5a; } 156 | -------------------------------------------------------------------------------- /dist/demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Wobble - Demo 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |

brusher

30 |

A lightweight library to create interactive backgrounds

31 |
32 | 33 | ← Back to Home 34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /dist/demo/images/brush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/brush.png -------------------------------------------------------------------------------- /dist/demo/images/brush.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 72 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /dist/demo/images/default-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/default-blurred.jpg -------------------------------------------------------------------------------- /dist/demo/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/default.jpg -------------------------------------------------------------------------------- /dist/demo/images/github.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/github.svg -------------------------------------------------------------------------------- /dist/demo/images/homepage-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/homepage-blurred.jpg -------------------------------------------------------------------------------- /dist/demo/images/homepage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/homepage.jpg -------------------------------------------------------------------------------- /dist/demo/images/keep-cleared-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/keep-cleared-blurred.jpg -------------------------------------------------------------------------------- /dist/demo/images/keep-cleared.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/keep-cleared.jpg -------------------------------------------------------------------------------- /dist/demo/images/line-style-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/line-style-blurred.jpg -------------------------------------------------------------------------------- /dist/demo/images/line-style.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/line-style.jpg -------------------------------------------------------------------------------- /dist/demo/images/non-sticky-blurred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/non-sticky-blurred.jpg -------------------------------------------------------------------------------- /dist/demo/images/non-sticky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/non-sticky.jpg -------------------------------------------------------------------------------- /dist/demo/images/separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamranahmedse/brusher/a54e30f6b5abb1fe43386a63ed03a07bd9333ae9/dist/demo/images/separator.png -------------------------------------------------------------------------------- /dist/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Brusher - Create Interactive Backgrounds for Webpages 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |

brusher

30 |

A lightweight library to create interactive backgrounds

31 | 32 | Star 33 |
34 |
35 | 36 |
37 |

Background of this page has been auto created by brusher. Move your mouse around to see it in action.

38 |

All you need is an image. Below code shows how the current page has been initialized

39 |
const brusher = new Brusher({
40 |   image: 'abstract.png'
41 | });
42 | 
43 | brusher.init();
44 | 
45 | 46 |

There are several options that you can use to modify the result. Have a look at the section below.

47 |
48 | 49 |
50 |

Available Options

51 |

Here is the list of options that you may use

52 |
const brusher = new Brusher({
53 |   image: 'abstract.png', // Path of the image to be used as a brush
54 |   keepCleared: false, // Whether to keep cleared sections or blur them again
55 |   stroke: 80, // Stroke size for the brush
56 |   lineStyle: 'round', // Brush style (round, square, butt)
57 |   autoBlur: false, // Brusher will use the provided image for the blurry background
58 |   autoBlurValue: 15, // Blur strength in pixels
59 | });
60 | 
61 | brusher.init();
62 | 
63 | 64 |

Have a look at the github page for further details.

65 |
66 | 67 |
68 |

Examples

69 |

Find some of the examples demonstrating some of the options in use through the buttons below.

70 | 71 | Keep Cleared Sections 72 | Without Background Demo 73 | Sticky Blur Demo 74 | Different Line Style 75 | 76 | Github 77 | 78 | 79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /dist/demo/keep-cleared.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Brusher - Create Interactive Backgrounds for Webpages 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |

Preserve Cleared

30 |

If you want brusher to preserve the cleared sections, set keepCleared to true

31 |
32 | 33 |
const brusher = new Brusher({
34 |   image: 'path/to/image.png',
35 |   keepCleared: true,
36 | });
37 | 
38 | brusher.init();
39 | 
40 | 41 | ← Back to Home 42 |
43 |
44 |

Examples

45 |

Find some of the examples demonstrating some of the options in use through the buttons below.

46 | 47 | Keep Cleared Sections 48 | Without Background Demo 49 | Sticky Blur Demo 50 | Different Line Style 51 | 52 | Github 53 | 54 | 55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /dist/demo/line-style.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Brusher - Create Interactive Backgrounds for Webpages 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |

Line Styles

30 |

There are three different line styles that brusher understands i.e. round, square, butt. If you don't provide it, it will use round by default. Also, depending upon the image, the effect might not be quite visible in the background.

31 |
32 | 33 |
const brusher = new Brusher({
34 |   image: 'path/to/image.png',
35 |   lineStyle: 'butt',
36 | });
37 | 
38 | brusher.init();
39 | 
40 | ← Back to Home 41 |
42 |
43 |

Examples

44 |

Find some of the examples demonstrating some of the options in use through the buttons below.

45 | 46 | Keep Cleared Sections 47 | Without Background Demo 48 | Sticky Blur Demo 49 | Different Line Style 50 | 51 | Github 52 | 53 | 54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /dist/demo/non-sticky.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Wobble - Demo 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |

brusher

30 |

A lightweight library to create interactive backgrounds

31 |
32 | 33 | ← Back to Home 34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /dist/demo/sticky.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Brusher - Create Interactive Backgrounds for Webpages 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |

Sticky Blur

30 |

If you want the cleared sections to be not preserved, all you have to do is set keepCleared to false; or not set it since it defaults to false

31 |
32 | 33 |
const brusher = new Brusher({
34 |   image: 'path/to/image.png',
35 |   keepCleared: false,
36 | });
37 | 
38 | brusher.init();
39 | 
40 | ← Back to Home 41 |
42 |
43 |

Examples

44 |

Find some of the examples demonstrating some of the options in use through the buttons below.

45 | 46 | Keep Cleared Sections 47 | Without Background Demo 48 | Sticky Blur Demo 49 | Different Line Style 50 | 51 | Github 52 | 53 | 54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Kamran Ahmed 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brusher", 3 | "version": "0.1.4", 4 | "description": "A light-weight, vanilla JavaScript library to help you create fancy backgrounds", 5 | "main": "dist/brusher.min.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "build-demo": "NODE_ENV=production webpack --config webpack.config.demo.js", 9 | "build": "webpack --config webpack.config.prod.js", 10 | "push-demo": "git subtree push --prefix dist/demo origin gh-pages" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/kamranahmedse/brusher/issues" 14 | }, 15 | "homepage": "https://github.com/kamranahmedse/brusher#readme", 16 | "repository": "https://github.com/kamranahmedse/brusher", 17 | "author": "Kamran Ahmed ", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@babel/core": "^7.0.0", 21 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 22 | "@babel/preset-env": "^7.0.0", 23 | "babel-eslint": "^10.0.1", 24 | "babel-loader": "^8.0.6", 25 | "babel-plugin-add-module-exports": "^1.0.2", 26 | "copy-webpack-plugin": "^5.0.3", 27 | "css-loader": "^3.0.0", 28 | "eslint": "^5.16.0", 29 | "eslint-config-airbnb-base": "^13.1.0", 30 | "eslint-loader": "^2.1.2", 31 | "eslint-plugin-import": "^2.17.3", 32 | "eslint-plugin-node": "^9.1.0", 33 | "extract-loader": "^3.1.0", 34 | "extract-text-webpack-plugin": "next", 35 | "file-loader": "^4.0.0", 36 | "html-loader": "^0.5.5", 37 | "html-webpack-plugin": "^3.2.0", 38 | "node-sass": "^4.12.0", 39 | "opn": "^6.0.0", 40 | "postcss-loader": "^3.0.0", 41 | "sass-loader": "^7.1.0", 42 | "style-loader": "^0.23.1", 43 | "webpack": "^4.34.0", 44 | "webpack-cli": "^3.3.4", 45 | "webpack-dev-server": "^3.5.1" 46 | }, 47 | "dependencies": {} 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![](https://i.imgur.com/Tq7TBnA.png) 2 | 3 | > Little vanilla JS library to add interactive backgrounds to your webpages - [View Demo](http://kamranahmed.info/brusher) 4 | 5 | ## Installation 6 | 7 | Install it using yarn or npm 8 | ```bash 9 | yarn add brusher 10 | ``` 11 | Or you may use unpkg 12 | ``` 13 | http://unpkg.com/brusher/dist/brusher.min.js 14 | ``` 15 | 16 | ## Usage 17 | 18 | For the basic usage, all you need to do is create an instance of `Brusher` and provide an image 19 | 20 | ```javascript 21 | import Brusher from 'brusher'; 22 | 23 | const brusher = new Brusher({ 24 | image: 'abstract.png' 25 | }); 26 | 27 | brusher.init(); 28 | ``` 29 | 30 | ## Available Options 31 | 32 | Here is the list of options that you may use 33 | 34 | ```javascript 35 | const brusher = new Brusher({ 36 | image: 'abstract.png', // Path of the image to be used as a brush 37 | keepCleared: true, // Put the blur back after user has cleared it 38 | stroke: 80, // Stroke size for the brush 39 | lineStyle: 'round', // Brush style (round, square, butt) 40 | autoBlur: false, // Brusher will use the provided image for the blurry background 41 | autoBlurValue: 15, // Blur strength in pixels 42 | }); 43 | 44 | brusher.init(); 45 | ``` 46 | 47 | A note on blurry background: although brusher is capable of generating blurry background by itself, it is recommended that you [blur the image yourself](http://pinetools.com/blur-image) and apply it to the body for improved performance. Brusher relies on CSS blur for the background. And rendering performance for the pre-provided blurred image would be of-course much better than that applied using CSS. Here is the sample CSS that you may use for the background 48 | 49 | ```css 50 | body { 51 | background-size: cover; 52 | background-position: 0 0; 53 | background-attachment: fixed; 54 | background-image: url(path/to/blurred/image.jpg); 55 | } 56 | ``` 57 | 58 | ## License 59 | 60 | MIT © [Kamran Ahmed](https://twitter.com/kamranahmedse) 61 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const WebpackDevServer = require('webpack-dev-server'); 3 | const opn = require('opn'); 4 | 5 | const config = require('./webpack.config.demo'); 6 | 7 | const PORT = 3000; 8 | const HOST = 'localhost'; 9 | const URL = `http://${HOST}:${PORT}`; 10 | 11 | new WebpackDevServer(webpack(config), { 12 | publicPath: config.output.publicPath, 13 | }).listen(PORT, HOST, (error, result) => { 14 | if (error) { 15 | console.error(error); 16 | } 17 | 18 | opn(URL); 19 | console.log(`Listening at ${URL}`); 20 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default class Brusher { 2 | /** 3 | * @param {Object} options 4 | */ 5 | constructor(options = {}) { 6 | this.blurryStyleNode = null; 7 | this.mouseSteps = []; 8 | this.drawBoardCanvas = null; 9 | this.drawBoardCanvasContext = null; 10 | this.imageCanvas = null; 11 | this.imageCanvasContext = null; 12 | this.image = null; 13 | this.tailAnimationFrame = null; 14 | 15 | this.options = { 16 | image: null, 17 | stroke: 80, 18 | keepCleared: false, 19 | autoBlur: false, 20 | autoBlurValue: 15, 21 | lineStyle: 'round', 22 | ...options, 23 | element: 'body', // Only `body` element is supported for now 24 | }; 25 | 26 | this.drawTail = this.drawTail.bind(this); 27 | this.validateOptions(); 28 | } 29 | 30 | /** 31 | * Validates for any invalid options given 32 | */ 33 | validateOptions() { 34 | if (!this.options.image) { 35 | throw new Error('Image path to use as brush is required'); 36 | } 37 | } 38 | 39 | /** 40 | * Initializes the brusher, while preparing the 41 | * canvas and binding the necessary events 42 | */ 43 | init() { 44 | if (!Brusher.isCanvasSupported()) { 45 | return; 46 | } 47 | 48 | this.prepareCanvas(); 49 | this.bind(); 50 | } 51 | 52 | bind() { 53 | document.addEventListener('mousemove', (e) => { 54 | if (!e.clientX || !e.clientY) { 55 | return; 56 | } 57 | 58 | // Keep the co-ordinates and time, this will be used while drawing 59 | // the stroke. Time helps us decrease the length of stroke over time. 60 | this.mouseSteps.unshift({ 61 | time: Date.now(), 62 | ...this.getMousePositionInCanvas(e), 63 | }); 64 | 65 | this.drawTail(); 66 | }); 67 | 68 | window.addEventListener('resize', () => { 69 | this.prepareCanvas(); 70 | }); 71 | } 72 | 73 | /** 74 | * Gets the mouse position relative to canvas 75 | * @param evMouseMove 76 | * @returns {{x: number, y: number}} 77 | */ 78 | getMousePositionInCanvas(evMouseMove) { 79 | const canvasRect = this.drawBoardCanvas.getBoundingClientRect(); 80 | 81 | return { 82 | x: evMouseMove.clientX - canvasRect.left, 83 | y: evMouseMove.clientY - canvasRect.top, 84 | }; 85 | } 86 | 87 | /** 88 | * Prepares the canvases i.e. one upon which we will draw the image 89 | * and the other holds just the image for us to draw on to the 90 | * drawing canvas 91 | */ 92 | prepareCanvas() { 93 | if (this.options.autoBlur) { 94 | this.attachBlurryBackground(); 95 | } 96 | 97 | this.prepareDrawingCanvas(); 98 | this.prepareImageCanvas(); 99 | this.loadSelectedImage(); 100 | } 101 | 102 | /** 103 | * Creates a blurry background for the body if needed 104 | * @todo use CSS file and just apply a class 105 | */ 106 | attachBlurryBackground() { 107 | if (this.blurryStyleNode) { 108 | return; 109 | } 110 | 111 | const blurryCss = ` 112 | body { position: relative; } 113 | body:before { 114 | background-size: cover; 115 | background-position: 0 0; 116 | background-attachment: fixed; 117 | content: ''; 118 | background-image: url('${this.options.image}'); 119 | position: fixed; 120 | z-index: -1; 121 | display: block; 122 | width: 100%; 123 | height: 100%; 124 | -webkit-filter: blur(${this.options.autoBlurValue}px); 125 | filter: blur(${this.options.autoBlurValue}px); 126 | top: 0; 127 | left: 0; 128 | } 129 | `; 130 | 131 | const style = document.createElement('style'); 132 | style.type = 'text/css'; 133 | style.append(document.createTextNode(blurryCss)); 134 | document.head.appendChild(style); 135 | 136 | this.blurryStyleNode = style; 137 | } 138 | 139 | /** 140 | * Prepares the canvas attached to the document, upon which we 141 | * will draw the non-blurred image 142 | */ 143 | prepareDrawingCanvas() { 144 | if (this.drawBoardCanvas && this.drawBoardCanvas.parentElement) { 145 | this.drawBoardCanvas.parentElement.removeChild(this.drawBoardCanvas); 146 | } 147 | 148 | const canvas = this.createCanvasNode(); 149 | 150 | canvas.style.position = 'fixed'; 151 | canvas.style.top = '0'; 152 | canvas.style.left = '0'; 153 | canvas.style.zIndex = '-1'; 154 | 155 | this.drawBoardCanvas = canvas; 156 | this.drawBoardCanvasContext = canvas.getContext('2d'); 157 | 158 | document.body.appendChild(this.drawBoardCanvas); 159 | } 160 | 161 | /** 162 | * Prepares the canvas for the image which will help us draw the colored 163 | * image on top of the blurred image/empty background 164 | */ 165 | prepareImageCanvas() { 166 | const canvas = this.createCanvasNode(); 167 | 168 | this.imageCanvas = canvas; 169 | this.imageCanvasContext = canvas.getContext('2d'); 170 | 171 | this.imageCanvasContext.lineCap = this.options.lineStyle; 172 | this.imageCanvasContext.shadowBlur = 30; 173 | this.imageCanvasContext.shadowColor = '#000000'; 174 | } 175 | 176 | /** 177 | * Loads the given image in the virtual image property and 178 | * draws the tail if necessary 179 | */ 180 | loadSelectedImage() { 181 | if (this.image) { 182 | return; 183 | } 184 | 185 | this.image = new Image(); 186 | this.image.addEventListener('load', () => this.drawTail()); 187 | this.image.addEventListener('error', () => console.error('Failed to load image')); 188 | 189 | this.image.src = this.options.image; 190 | } 191 | 192 | /** 193 | * Draws tail at the path where mouse was last moved 194 | */ 195 | drawTail() { 196 | this.removeOldSteps(); 197 | 198 | window.cancelAnimationFrame(this.tailAnimationFrame); 199 | if (this.mouseSteps.length > 0) { 200 | this.tailAnimationFrame = window.requestAnimationFrame(this.drawTail); 201 | } 202 | 203 | // Do not clear the drawn image if the blur is to be kept 204 | if (!this.options.keepCleared) { 205 | this.imageCanvasContext.clearRect(0, 0, this.imageCanvas.width, this.imageCanvas.height); 206 | } 207 | 208 | this.createStrokeFromSteps(); 209 | 210 | let drawHeight = (this.drawBoardCanvas.width / this.image.naturalWidth) * this.image.naturalHeight; 211 | let drawWidth = this.drawBoardCanvas.width; 212 | 213 | if (drawHeight < this.drawBoardCanvas.height) { 214 | drawHeight = this.drawBoardCanvas.height; 215 | drawWidth = (this.drawBoardCanvas.height / this.image.naturalHeight) * this.image.naturalWidth; 216 | } 217 | 218 | this.drawBoardCanvasContext.drawImage(this.image, 0, 0, drawWidth, drawHeight); 219 | this.drawBoardCanvasContext.globalCompositeOperation = 'destination-in'; 220 | this.drawBoardCanvasContext.drawImage(this.imageCanvas, 0, 0); 221 | this.drawBoardCanvasContext.globalCompositeOperation = 'source-over'; 222 | } 223 | 224 | /** 225 | * Creates stroke from the recorded mouse steps 226 | */ 227 | createStrokeFromSteps() { 228 | const currentTime = Date.now(); 229 | 230 | for (let counter = 1; counter < this.mouseSteps.length; counter++) { 231 | const timeDiff = (currentTime - this.mouseSteps[counter].time) / 1000; 232 | const strokeAlpha = Math.max(1 - timeDiff, 0); 233 | 234 | this.imageCanvasContext.strokeStyle = `rgba(0,0,0,${strokeAlpha})`; 235 | this.imageCanvasContext.lineWidth = this.options.stroke; 236 | this.imageCanvasContext.beginPath(); 237 | this.imageCanvasContext.moveTo(this.mouseSteps[counter - 1].x, this.mouseSteps[counter - 1].y); 238 | this.imageCanvasContext.lineTo(this.mouseSteps[counter].x, this.mouseSteps[counter].y); 239 | this.imageCanvasContext.stroke(); 240 | } 241 | } 242 | 243 | /** 244 | * Remove any steps older than one second to not keep 245 | * them piling up in memory 246 | */ 247 | removeOldSteps() { 248 | const currentTimeStamp = Date.now(); 249 | 250 | for (let counter = 0; counter < this.mouseSteps.length; counter++) { 251 | if (currentTimeStamp - this.mouseSteps[counter].time > 1000) { 252 | this.mouseSteps.length = counter; 253 | } 254 | } 255 | } 256 | 257 | /** 258 | * Creates canvas for the set element 259 | * @returns {HTMLCanvasElement} 260 | */ 261 | createCanvasNode() { 262 | const elementDimensions = this.getElementDimensions(); 263 | 264 | const canvas = document.createElement('canvas'); 265 | canvas.width = elementDimensions.width; 266 | canvas.height = elementDimensions.height; 267 | 268 | return canvas; 269 | } 270 | 271 | /** 272 | * Gets node from the given query selector 273 | * @returns {Object|Element} 274 | */ 275 | getElementNode() { 276 | const element = this.options.element; 277 | if (element && typeof element === 'object' && 'nodeType' in element) { 278 | return element; 279 | } 280 | 281 | return document.querySelector(element || 'body'); 282 | } 283 | 284 | /** 285 | * Gets dimensions for the selected element 286 | * @returns {ClientRect | DOMRect} 287 | */ 288 | getElementDimensions() { 289 | return this.getElementNode().getBoundingClientRect(); 290 | } 291 | 292 | /** 293 | * Checks if canvas is supported or not 294 | * @returns {boolean} 295 | */ 296 | static isCanvasSupported() { 297 | const elem = document.createElement('canvas'); 298 | return !!(elem.getContext && elem.getContext('2d')); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /webpack.config.demo.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const isProduction = process.env.NODE_ENV === 'production'; 7 | const scriptFileName = isProduction ? 'brusher-demo.min.js' : 'brusher-demo.js'; 8 | const styleFileName = isProduction ? 'brusher-demo.min.css' : 'brusher-demo.css'; 9 | 10 | module.exports = { 11 | mode: isProduction ? 'production' : 'development', 12 | entry: [ 13 | // Add dev server only for non-production environment 14 | ...(!isProduction ? ['webpack-dev-server/client?http://localhost:3000'] : []), 15 | './demo/styles/demo.scss', 16 | './demo/scripts/demo.js', 17 | './src/index.js', 18 | ], 19 | output: { 20 | path: path.join(__dirname, '/dist/demo'), 21 | filename: scriptFileName, 22 | libraryTarget: 'umd', 23 | library: 'Brusher', 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.js$/, 29 | exclude: /node_modules/, 30 | loader: 'eslint-loader', 31 | enforce: 'pre', 32 | options: { 33 | failOnWarning: false, 34 | failOnError: true, 35 | }, 36 | }, 37 | { 38 | test: /\.js$/, 39 | exclude: /node_modules/, 40 | loader: 'babel-loader', 41 | options: { 42 | presets: [ 43 | [ 44 | '@babel/preset-env', 45 | { 46 | corejs: '2', 47 | useBuiltIns: 'usage', 48 | }, 49 | ], 50 | ], 51 | plugins: [ 52 | 'add-module-exports', 53 | '@babel/plugin-proposal-object-rest-spread', 54 | ], 55 | }, 56 | }, 57 | { 58 | test: /.scss$/, 59 | loader: ExtractTextPlugin.extract([ 60 | { 61 | loader: 'css-loader', 62 | options: { url: false }, 63 | }, 64 | 'sass-loader', 65 | ]), 66 | }, 67 | ], 68 | }, 69 | plugins: [ 70 | new ExtractTextPlugin({ 71 | filename: styleFileName, 72 | allChunks: true, 73 | }), 74 | new CopyWebpackPlugin([ 75 | { from: './demo/images', to: 'images' }, 76 | ]), 77 | new HtmlWebpackPlugin({ 78 | bodyClass: 'homepage', 79 | template: 'demo/homepage.html', 80 | filename: 'index.html', 81 | }), 82 | new HtmlWebpackPlugin({ 83 | template: 'demo/sticky.html', 84 | filename: 'sticky.html', 85 | }), 86 | new HtmlWebpackPlugin({ 87 | template: 'demo/keep-cleared.html', 88 | filename: 'keep-cleared.html', 89 | }), 90 | new HtmlWebpackPlugin({ 91 | template: 'demo/line-style.html', 92 | filename: 'line-style.html', 93 | }), 94 | new HtmlWebpackPlugin({ 95 | template: 'demo/background-less.html', 96 | filename: 'background-less.html', 97 | }), 98 | ], 99 | stats: { 100 | colors: true, 101 | }, 102 | devtool: 'cheap-module-eval-source-map', 103 | }; 104 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: [ 6 | './src/index.js', 7 | ], 8 | output: { 9 | path: path.join(__dirname, '/dist'), 10 | // publicPath: '/dist/', 11 | filename: 'brusher.min.js', 12 | libraryTarget: 'umd', 13 | library: 'Brusher', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | exclude: /node_modules/, 20 | loader: 'eslint-loader', 21 | enforce: 'pre', 22 | options: { 23 | failOnWarning: false, 24 | failOnError: true, 25 | }, 26 | }, 27 | { 28 | test: /\.js$/, 29 | exclude: /node_modules/, 30 | loader: 'babel-loader', 31 | options: { 32 | presets: [ 33 | [ 34 | '@babel/preset-env', 35 | { 36 | corejs: '2', 37 | useBuiltIns: 'usage', 38 | }, 39 | ], 40 | ], 41 | plugins: [ 42 | 'add-module-exports', 43 | '@babel/plugin-proposal-object-rest-spread', 44 | ], 45 | }, 46 | }, 47 | ], 48 | }, 49 | plugins: [], 50 | stats: { 51 | colors: true, 52 | }, 53 | devtool: 'cheap-module-source-map', 54 | }; 55 | --------------------------------------------------------------------------------