├── .npmignore ├── docs ├── bubbly.gif ├── img │ ├── config0.png │ ├── config1.png │ ├── config2.png │ ├── config3.png │ ├── config4.png │ └── config5.png ├── bubbly-bg.js ├── main.js ├── styles.css └── index.html ├── minify.js ├── package.json ├── .gitignore ├── dist └── bubbly-bg.js ├── index.d.ts ├── src └── bubbly-bg.js ├── README.md └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .npmignore 3 | gulpfile.js 4 | docs 5 | -------------------------------------------------------------------------------- /docs/bubbly.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/bubbly-bg/HEAD/docs/bubbly.gif -------------------------------------------------------------------------------- /docs/img/config0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/bubbly-bg/HEAD/docs/img/config0.png -------------------------------------------------------------------------------- /docs/img/config1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/bubbly-bg/HEAD/docs/img/config1.png -------------------------------------------------------------------------------- /docs/img/config2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/bubbly-bg/HEAD/docs/img/config2.png -------------------------------------------------------------------------------- /docs/img/config3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/bubbly-bg/HEAD/docs/img/config3.png -------------------------------------------------------------------------------- /docs/img/config4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/bubbly-bg/HEAD/docs/img/config4.png -------------------------------------------------------------------------------- /docs/img/config5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/bubbly-bg/HEAD/docs/img/config5.png -------------------------------------------------------------------------------- /minify.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Terser = require("terser"); 3 | 4 | (async () => { 5 | let minified = (await Terser.minify(fs.readFileSync("src/bubbly-bg.js", "utf8"))).code; 6 | fs.writeFileSync("dist/bubbly-bg.js", minified, "utf8"); 7 | fs.writeFileSync("docs/bubbly-bg.js", minified, "utf8"); 8 | })(); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bubbly-bg", 3 | "version": "1.0.0", 4 | "description": "Lightweight and beautiful bubbly backgrounds in less than 1kB", 5 | "main": "dist/bubbly-bg.js", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "dist/bubbly-bg.js", 9 | "src/bubbly-bg.js", 10 | "index.d.ts" 11 | ], 12 | "keywords": [ 13 | "background", 14 | "canvas", 15 | "animation", 16 | "bubbles", 17 | "bubbly", 18 | "particles", 19 | "lightweight" 20 | ], 21 | "scripts": { 22 | "build": "node minify.js", 23 | "watch": "nodemon --watch src --exec 'npm run build'" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/tipsy/bubbly-bg.git" 28 | }, 29 | "author": "David Åse", 30 | "license": "Apache-2.0", 31 | "bugs": { 32 | "url": "https://github.com/tipsy/bubbly-bg/issues" 33 | }, 34 | "homepage": "https://github.com/tipsy/bubbly-bg#readme", 35 | "devDependencies": { 36 | "nodemon": "^3.1.10", 37 | "terser": "^5.44.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .cache 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | -------------------------------------------------------------------------------- /dist/bubbly-bg.js: -------------------------------------------------------------------------------- 1 | window.bubbly=function(t={}){const e=t.canvas??(()=>{let t=document.createElement("canvas");return t.setAttribute("style","position:fixed;z-index:-1;left:0;top:0;min-width:100vw;min-height:100vh;"),t.width=window.innerWidth,t.height=window.innerHeight,document.body.appendChild(t),t})(),o=e.getContext("2d"),{compose:a,bubbles:i,background:r,animate:n}={compose:t.compose??"lighter",bubbles:Object.assign({count:Math.floor(.02*(e.width+e.height)),radius:()=>4+Math.random()*window.innerWidth/25,fill:()=>`hsla(0, 0%, 100%, ${.1*Math.random()})`,angle:()=>Math.random()*Math.PI*2,velocity:()=>.1+.5*Math.random(),shadow:()=>null,stroke:()=>null},t.bubbles??{}),background:t.background??(()=>"#2AE"),animate:!1!==t.animate};i.objectCreator=t.bubbles?.objectCreator??(()=>({r:i.radius(),f:i.fill(),x:Math.random()*e.width,y:Math.random()*e.height,a:i.angle(),v:i.velocity(),sh:i.shadow(),st:i.stroke(),draw:(t,e)=>{e.sh&&(t.shadowColor=e.sh.color,t.shadowBlur=e.sh.blur),t.fillStyle=e.f,t.beginPath(),t.arc(e.x,e.y,e.r,0,2*Math.PI),t.fill(),e.st&&(t.strokeStyle=e.st.color,t.lineWidth=e.st.width,t.stroke())}}));let h=Array.from({length:i.count},i.objectCreator);requestAnimationFrame(function t(){if(null===e.parentNode)return h=[],cancelAnimationFrame(t);n&&requestAnimationFrame(t);o.globalCompositeOperation="source-over",o.fillStyle=r(o),o.fillRect(0,0,e.width,e.height),o.globalCompositeOperation=a;for(const t of h)t.draw(o,t),t.x+=Math.cos(t.a)*t.v,t.y+=Math.sin(t.a)*t.v,t.x-t.r>e.width&&(t.x=-t.r),t.x+t.r<0&&(t.x=e.width+t.r),t.y-t.r>e.height&&(t.y=-t.r),t.y+t.r<0&&(t.y=e.height+t.r)})}; -------------------------------------------------------------------------------- /docs/bubbly-bg.js: -------------------------------------------------------------------------------- 1 | window.bubbly=function(t={}){const e=t.canvas??(()=>{let t=document.createElement("canvas");return t.setAttribute("style","position:fixed;z-index:-1;left:0;top:0;min-width:100vw;min-height:100vh;"),t.width=window.innerWidth,t.height=window.innerHeight,document.body.appendChild(t),t})(),o=e.getContext("2d"),{compose:a,bubbles:i,background:r,animate:n}={compose:t.compose??"lighter",bubbles:Object.assign({count:Math.floor(.02*(e.width+e.height)),radius:()=>4+Math.random()*window.innerWidth/25,fill:()=>`hsla(0, 0%, 100%, ${.1*Math.random()})`,angle:()=>Math.random()*Math.PI*2,velocity:()=>.1+.5*Math.random(),shadow:()=>null,stroke:()=>null},t.bubbles??{}),background:t.background??(()=>"#2AE"),animate:!1!==t.animate};i.objectCreator=t.bubbles?.objectCreator??(()=>({r:i.radius(),f:i.fill(),x:Math.random()*e.width,y:Math.random()*e.height,a:i.angle(),v:i.velocity(),sh:i.shadow(),st:i.stroke(),draw:(t,e)=>{e.sh&&(t.shadowColor=e.sh.color,t.shadowBlur=e.sh.blur),t.fillStyle=e.f,t.beginPath(),t.arc(e.x,e.y,e.r,0,2*Math.PI),t.fill(),e.st&&(t.strokeStyle=e.st.color,t.lineWidth=e.st.width,t.stroke())}}));let h=Array.from({length:i.count},i.objectCreator);requestAnimationFrame(function t(){if(null===e.parentNode)return h=[],cancelAnimationFrame(t);n&&requestAnimationFrame(t);o.globalCompositeOperation="source-over",o.fillStyle=r(o),o.fillRect(0,0,e.width,e.height),o.globalCompositeOperation=a;for(const t of h)t.draw(o,t),t.x+=Math.cos(t.a)*t.v,t.y+=Math.sin(t.a)*t.v,t.x-t.r>e.width&&(t.x=-t.r),t.x+t.r<0&&(t.x=e.width+t.r),t.y-t.r>e.height&&(t.y=-t.r),t.y+t.r<0&&(t.y=e.height+t.r)})}; -------------------------------------------------------------------------------- /docs/main.js: -------------------------------------------------------------------------------- 1 | function configToText(config) { 2 | return ` 3 | bubbly({ 4 | canvas: ${config.cv}, 5 | compose: ${config.compose ? `"${config.compose}"` : undefined}, 6 | bubbles: { 7 | count: ${config.bubbles?.count}, 8 | radius: ${config.bubbles?.radius}, 9 | fill: ${config.bubbles?.fill}, 10 | angle: ${config.bubbles?.angle}, 11 | velocity: ${config.bubbles?.velocity}, 12 | shadow: ${config.bubbles?.shadow}, 13 | stroke: ${config.bubbles?.stroke}, 14 | objectCreator: ${formatFunction(config.bubbles?.objectCreator, 12)}, 15 | }, 16 | background: ${formatFunction(config.background, 8)}, 17 | animate: ${config.animate}, 18 | });`.trim() 19 | .split('\n') 20 | .filter(line => !line.includes(': undefined,')) // remove undefined values 21 | .join('\n'); 22 | } 23 | 24 | function formatFunction(func, targetIndent) { 25 | if (!func) return undefined 26 | if (func.toString().split("\n").length === 1) return func.toString(); 27 | const lines = func.toString().split("\n"); 28 | const indentOfFirstLine = lines[1].match(/^\s*/)[0].length; 29 | if (indentOfFirstLine === targetIndent) { 30 | return func.toString 31 | } else if (indentOfFirstLine < targetIndent) { 32 | const indent = targetIndent - indentOfFirstLine; 33 | return lines.map((line, i) => i === 0 ? line : " ".repeat(indent) + line).join('\n'); 34 | } else if (indentOfFirstLine > targetIndent) { 35 | const indent = indentOfFirstLine - targetIndent; 36 | return lines.map(line => line.replace(" ".repeat(indent), "")).join('\n'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | [v-cloak] { 2 | display: none; 3 | } 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | min-height: 100vh; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | } 15 | 16 | .overlay { 17 | background: rgba(255, 255, 255, 0.1); 18 | border: 1px solid rgba(255, 255, 255, 0.4); 19 | padding: 24px; 20 | text-align: center; 21 | border-radius: 4px; 22 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); 23 | backdrop-filter: blur(8px); 24 | max-width: 90vw; 25 | } 26 | 27 | .text, .text a { 28 | font: 16px monospace; 29 | font-weight: bold; 30 | color: black; 31 | text-shadow: 1px 1px 50px rgba(255, 255, 255, 1); 32 | } 33 | 34 | .text + .text { 35 | margin-top: 6px; 36 | } 37 | 38 | @media (min-width: 1100px) { 39 | .overlay { 40 | max-width: 800px; 41 | } 42 | } 43 | 44 | .examples { 45 | display: flex; 46 | flex-wrap: wrap; 47 | justify-content: space-between; 48 | } 49 | 50 | .example { 51 | width: calc(33% - 12px); 52 | margin-bottom: 24px; 53 | } 54 | 55 | @media (max-width: 480px) { 56 | .example { 57 | width: calc(50% - 12px); 58 | } 59 | } 60 | 61 | .example img { 62 | cursor: pointer; 63 | display: block; 64 | width: 100%; 65 | border-radius: 4px; 66 | } 67 | 68 | .example img:hover { 69 | filter: saturate(1.1) contrast(1.1); 70 | } 71 | 72 | .example { 73 | position: relative; 74 | } 75 | 76 | .example .copy { 77 | position: absolute; 78 | right: 0; 79 | top: 0; 80 | font: 14px monospace; 81 | color: white; 82 | background-color: rgba(0, 0, 0, 0.2); 83 | padding: 6px 10px; 84 | z-index: 1001; 85 | cursor: pointer; 86 | border: none; 87 | border-top-right-radius: 4px; 88 | border-bottom-left-radius: 4px; 89 | } 90 | 91 | .example .config:hover { 92 | background-color: rgba(0, 0, 0, 0.5); 93 | } 94 | 95 | .toast { 96 | background: rgba(0, 0, 0, 0.5) !important; 97 | } 98 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration options for bubbly-bg 3 | */ 4 | export interface BubblyConfig { 5 | /** 6 | * Canvas element to use. If not provided, a new canvas will be created and appended to the body. 7 | */ 8 | canvas?: HTMLCanvasElement; 9 | 10 | /** 11 | * Global composite operation for drawing bubbles. 12 | * @default "lighter" 13 | */ 14 | compose?: GlobalCompositeOperation; 15 | 16 | /** 17 | * Whether to animate the bubbles. 18 | * @default true 19 | */ 20 | animate?: boolean; 21 | 22 | /** 23 | * Background color or gradient function. 24 | * @default (ctx) => "#2AE" 25 | */ 26 | background?: string | ((ctx: CanvasRenderingContext2D) => string | CanvasGradient); 27 | 28 | /** 29 | * Bubble configuration options. 30 | */ 31 | bubbles?: { 32 | /** 33 | * Number of bubbles to create. 34 | * @default Math.floor((canvas.width + canvas.height) * 0.02) 35 | */ 36 | count?: number; 37 | 38 | /** 39 | * Function that returns the radius of a bubble. 40 | * @default () => 4 + Math.random() * window.innerWidth / 25 41 | */ 42 | radius?: () => number; 43 | 44 | /** 45 | * Function that returns the fill color of a bubble. 46 | * @default () => `hsla(0, 0%, 100%, ${Math.random() * 0.1})` 47 | */ 48 | fill?: () => string; 49 | 50 | /** 51 | * Function that returns the angle of a bubble's movement. 52 | * @default () => Math.random() * Math.PI * 2 53 | */ 54 | angle?: () => number; 55 | 56 | /** 57 | * Function that returns the velocity of a bubble. 58 | * @default () => 0.1 + Math.random() * 0.5 59 | */ 60 | velocity?: () => number; 61 | 62 | /** 63 | * Function that returns shadow configuration for a bubble. 64 | * @default () => null 65 | */ 66 | shadow?: () => { blur: number; color: string } | null; 67 | 68 | /** 69 | * Function that returns stroke configuration for a bubble. 70 | * @default () => null 71 | */ 72 | stroke?: () => { width: number; color: string } | null; 73 | 74 | /** 75 | * Advanced: Custom bubble object creator function. 76 | * Allows complete control over bubble creation and rendering. 77 | */ 78 | objectCreator?: () => BubbleObject; 79 | }; 80 | } 81 | 82 | /** 83 | * Bubble object interface for custom bubble creators 84 | */ 85 | export interface BubbleObject { 86 | /** Radius or size of the bubble */ 87 | r: number; 88 | /** Fill color */ 89 | f?: string; 90 | /** X position */ 91 | x: number; 92 | /** Y position */ 93 | y: number; 94 | /** Angle of movement */ 95 | a: number; 96 | /** Velocity */ 97 | v: number; 98 | /** Shadow configuration */ 99 | sh?: { blur: number; color: string } | null; 100 | /** Stroke configuration */ 101 | st?: { width: number; color: string } | null; 102 | /** Custom draw function */ 103 | draw: (ctx: CanvasRenderingContext2D, bubble: BubbleObject) => void; 104 | /** Any additional custom properties */ 105 | [key: string]: any; 106 | } 107 | 108 | /** 109 | * Creates a bubbly background animation 110 | * @param config - Configuration options 111 | */ 112 | export function bubbly(config?: BubblyConfig): void; 113 | 114 | declare global { 115 | interface Window { 116 | bubbly: typeof bubbly; 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/bubbly-bg.js: -------------------------------------------------------------------------------- 1 | window.bubbly = function (userConfig = {}) { 2 | // we need to create a canvas element if the user didn't provide one 3 | const cv = userConfig.canvas ?? (() => { 4 | let canvas = document.createElement("canvas"); 5 | canvas.setAttribute("style", "position:fixed;z-index:-1;left:0;top:0;min-width:100vw;min-height:100vh;"); 6 | canvas.width = window.innerWidth; 7 | canvas.height = window.innerHeight; 8 | document.body.appendChild(canvas); 9 | return canvas; 10 | })(); 11 | const ctx = cv.getContext("2d"); 12 | // we destructure the config object (with default values as fallback) 13 | const {compose, bubbles, background, animate} = { 14 | compose: userConfig.compose ?? "lighter", 15 | bubbles: Object.assign({ // default values 16 | count: Math.floor((cv.width + cv.height) * 0.02), 17 | radius: () => 4 + Math.random() * window.innerWidth / 25, 18 | fill: () => `hsla(0, 0%, 100%, ${Math.random() * 0.1})`, 19 | angle: () => Math.random() * Math.PI * 2, 20 | velocity: () => 0.1 + Math.random() * 0.5, 21 | shadow: () => null, // ({blur: 4, color: "#fff"}) 22 | stroke: () => null, // ({width: 2, color: "#fff"}) 23 | }, userConfig.bubbles ?? {}), 24 | background: userConfig.background ?? (() => "#2AE"), 25 | animate: userConfig.animate !== false, 26 | } 27 | // this function contains a lot of references to its parent scope, 28 | // so it must be defined after the config is created 29 | bubbles.objectCreator = userConfig.bubbles?.objectCreator ?? (() => ({ 30 | r: bubbles.radius(), 31 | f: bubbles.fill(), 32 | x: Math.random() * cv.width, 33 | y: Math.random() * cv.height, 34 | a: bubbles.angle(), 35 | v: bubbles.velocity(), 36 | sh: bubbles.shadow(), 37 | st: bubbles.stroke(), 38 | draw: (ctx, bubble) => { 39 | if (bubble.sh) { 40 | ctx.shadowColor = bubble.sh.color; 41 | ctx.shadowBlur = bubble.sh.blur; 42 | } 43 | ctx.fillStyle = bubble.f; 44 | ctx.beginPath(); 45 | ctx.arc(bubble.x, bubble.y, bubble.r, 0, Math.PI * 2); 46 | ctx.fill(); 47 | if (bubble.st) { 48 | ctx.strokeStyle = bubble.st.color; 49 | ctx.lineWidth = bubble.st.width; 50 | ctx.stroke(); 51 | } 52 | } 53 | })); 54 | let bubbleArray = Array.from({length: bubbles.count}, bubbles.objectCreator); 55 | requestAnimationFrame(draw); 56 | function draw() { 57 | if (cv.parentNode === null) { 58 | bubbleArray = []; 59 | return cancelAnimationFrame(draw); 60 | } 61 | if (animate) { 62 | requestAnimationFrame(draw); 63 | } 64 | ctx.globalCompositeOperation = "source-over"; 65 | ctx.fillStyle = background(ctx); 66 | ctx.fillRect(0, 0, cv.width, cv.height); 67 | ctx.globalCompositeOperation = compose; 68 | for (const bubble of bubbleArray) { 69 | bubble.draw(ctx, bubble); 70 | bubble.x += Math.cos(bubble.a) * bubble.v; 71 | bubble.y += Math.sin(bubble.a) * bubble.v; 72 | if (bubble.x - bubble.r > cv.width) { 73 | bubble.x = -bubble.r; 74 | } 75 | if (bubble.x + bubble.r < 0) { 76 | bubble.x = cv.width + bubble.r; 77 | } 78 | if (bubble.y - bubble.r > cv.height) { 79 | bubble.y = -bubble.r; 80 | } 81 | if (bubble.y + bubble.r < 0) { 82 | bubble.y = cv.height + bubble.r; 83 | } 84 | } 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bubbly demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 |
Click to change examples
23 |
github.com/tipsy/bubbly-bg
24 |
25 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🫧 bubbly-bg 4 | 5 | **Beautiful bubbly backgrounds in less than 1kB gzipped** 6 | 7 | [![npm version](https://img.shields.io/npm/v/bubbly-bg.svg)](https://www.npmjs.com/package/bubbly-bg) 8 | [![jsDelivr downloads](https://data.jsdelivr.com/v1/package/npm/bubbly-bg/badge)](https://www.jsdelivr.com/package/npm/bubbly-bg) 9 | [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) 10 | [![Bundle Size](https://img.shields.io/badge/gzipped-841%20bytes-brightgreen.svg)](dist/bubbly-bg.js) 11 | 12 | [**Live Demo**](https://tipsy.github.io/bubbly-bg) 13 | 14 | ![Bubbly animated](https://tipsy.github.io/bubbly-bg/bubbly.gif) 15 | 16 |
17 | 18 | --- 19 | 20 | ## ✨ Features 21 | 22 | - 🪶 **Lightweight** - Only 1.6kB minified, 841 bytes gzipped 23 | - 🎨 **Customizable** - Full control over colors, gradients, and animations 24 | - 🚀 **Zero dependencies** - Pure vanilla JavaScript 25 | - 📦 **Easy to use** - Just include and call `bubbly()` 26 | - 🎯 **TypeScript support** - Full type definitions included 27 | - 🔧 **Flexible** - Works with your own canvas or creates one automatically 28 | 29 | ## 🚀 Quick Start 30 | 31 | Add bubbly to your webpage and call `bubbly()`: 32 | 33 | ```html 34 | 35 | ... 36 | 37 | 38 | 39 | ``` 40 | 41 | That's it! Bubbly creates a `canvas` element with `position: fixed` and `z-index: -1`, filling the viewport. It's plug-and-play for most projects. 42 | 43 | ### Custom Canvas 44 | 45 | You can also use your own canvas element: 46 | 47 | ```javascript 48 | bubbly({ canvas: document.querySelector("#my-canvas") }); 49 | ``` 50 | 51 | ## 📦 Installation 52 | 53 | ### CDN 54 | ```html 55 | 56 | ``` 57 | 58 | ### npm 59 | ```bash 60 | npm install bubbly-bg 61 | ``` 62 | 63 | ### Direct Download 64 | [Download bubbly-bg.js](https://raw.githubusercontent.com/tipsy/bubbly-bg/master/dist/bubbly-bg.js) 65 | 66 | ## ⚙️ Configuration 67 | 68 | ### All Options 69 | 70 | ```javascript 71 | bubbly({ 72 | canvas: document.querySelector("#background"), // default is created and attached automatically 73 | compose: "lighter", // default is "lighter" 74 | animate: false, // default is true 75 | background: (ctx) => "#2AE", // default background, can be a color string or function returning gradient 76 | bubbles: { 77 | count: 100, // default is Math.floor((canvas.width + canvas.height) * 0.02) 78 | radius: () => 4 + Math.random() * 25, // default is () => 4 + Math.random() * window.innerWidth / 25 79 | fill: () => `hsla(${Math.random() * 360}, 100%, 50%, ${Math.random() * 0.25})`, // default is () => `hsla(0, 0%, 100%, ${Math.random() * 0.1})` 80 | angle: () => Math.random() * Math.PI * 2, // default is this 81 | velocity: () => 0.1 + Math.random() * 0.5, // default is this 82 | shadow: () => ({blur: 4, color: "#fff"}), // default is () => null 83 | stroke: () => ({width: 2, color: "#fff"}), // default is () => null 84 | objectCreator: () => ({...}) // advanced: custom bubble object creator 85 | } 86 | }); 87 | ``` 88 | 89 | ### Configuration Options 90 | 91 | | Option | Type | Default | Description | 92 | |--------|------|---------|-------------| 93 | | `canvas` | `HTMLCanvasElement` | auto-created | Canvas element to use | 94 | | `compose` | `string` | `"lighter"` | Global composite operation | 95 | | `animate` | `boolean` | `true` | Whether to animate bubbles | 96 | | `background` | `string \| function` | `"#2AE"` | Background color or gradient function | 97 | | `bubbles.count` | `number` | auto-calculated | Number of bubbles | 98 | | `bubbles.radius` | `function` | auto-calculated | Function returning bubble radius | 99 | | `bubbles.fill` | `function` | white with opacity | Function returning bubble fill color | 100 | | `bubbles.angle` | `function` | random | Function returning movement angle | 101 | | `bubbles.velocity` | `function` | random | Function returning velocity | 102 | | `bubbles.shadow` | `function` | `null` | Function returning shadow config | 103 | | `bubbles.stroke` | `function` | `null` | Function returning stroke config | 104 | | `bubbles.objectCreator` | `function` | default creator | Advanced: custom bubble creator | 105 | 106 | ## 🎨 Examples 107 | 108 | ### Default (Blue with white bubbles) 109 | ```javascript 110 | bubbly(); 111 | ``` 112 | 113 | ### Black/Red with red bubbles 114 | ```javascript 115 | bubbly({ 116 | background: (ctx) => { 117 | const gradient = ctx.createLinearGradient(0, 0, ctx.canvas.width, ctx.canvas.height); 118 | gradient.addColorStop(0, "#111"); 119 | gradient.addColorStop(1, "#422"); 120 | return gradient; 121 | }, 122 | bubbles: { 123 | fill: () => `hsla(0, 100%, 50%, ${Math.random() * 0.25})`, 124 | shadow: () => ({blur: 4, color: "#fff"}) 125 | } 126 | }); 127 | ``` 128 | 129 | ### Purple with multicolored bubbles 130 | ```javascript 131 | bubbly({ 132 | background: (ctx) => { 133 | const gradient = ctx.createLinearGradient(0, 0, ctx.canvas.width, ctx.canvas.height); 134 | gradient.addColorStop(0, "#4c004c"); 135 | gradient.addColorStop(1, "#1a001a"); 136 | return gradient; 137 | }, 138 | bubbles: { 139 | fill: () => `hsla(${Math.random() * 360}, 100%, 50%, ${Math.random() * 0.25})`, 140 | shadow: () => ({blur: 4, color: "#fff"}) 141 | } 142 | }); 143 | ``` 144 | 145 | ### Yellow/Pink with red/orange/yellow bubbles 146 | ```javascript 147 | bubbly({ 148 | background: (ctx) => { 149 | const gradient = ctx.createLinearGradient(0, 0, ctx.canvas.width, ctx.canvas.height); 150 | gradient.addColorStop(0, "#fff4e6"); 151 | gradient.addColorStop(1, "#ffe9e4"); 152 | return gradient; 153 | }, 154 | compose: "source-over", 155 | bubbles: { 156 | fill: () => `hsla(${Math.random() * 50}, 100%, 50%, .3)`, 157 | shadow: () => ({blur: 1, color: "#fff"}) 158 | } 159 | }); 160 | ``` 161 | 162 | ## 🔧 Advanced Usage 163 | 164 | ### Custom Bubble Creator 165 | 166 | For complete control over bubble creation and rendering: 167 | 168 | ```javascript 169 | bubbly({ 170 | bubbles: { 171 | objectCreator: function() { 172 | return { 173 | x: Math.random() * canvas.width, 174 | y: Math.random() * canvas.height, 175 | r: 10, 176 | a: Math.random() * Math.PI * 2, 177 | v: 1, 178 | draw: (ctx, bubble) => { 179 | // Custom drawing logic 180 | ctx.fillStyle = "red"; 181 | ctx.fillRect(bubble.x, bubble.y, bubble.r, bubble.r); 182 | } 183 | }; 184 | } 185 | } 186 | }); 187 | ``` 188 | 189 | ## 📝 TypeScript 190 | 191 | Full TypeScript definitions are included: 192 | 193 | ```typescript 194 | import { bubbly, BubblyConfig } from 'bubbly-bg'; 195 | 196 | const config: BubblyConfig = { 197 | animate: true, 198 | bubbles: { 199 | count: 50 200 | } 201 | }; 202 | 203 | bubbly(config); 204 | ``` 205 | 206 | ## 📄 License 207 | 208 | Apache-2.0 © [David Åse](https://github.com/tipsy) 209 | 210 | ## 🤝 Contributing 211 | 212 | Contributions, issues and feature requests are welcome! 213 | 214 | ## ⭐ Show your support 215 | 216 | Give a ⭐️ if this project helped you! 217 | 218 | --- 219 | 220 |
221 | 222 | Made with ❤️ by [David Åse](https://github.com/tipsy) 223 | 224 |
225 | 226 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 David Åse 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------