Skip to content

Commit b27dbf3

Browse files
authored
feat: text balloons
2 parents 6f38923 + 619c3b3 commit b27dbf3

File tree

8 files changed

+921
-413
lines changed

8 files changed

+921
-413
lines changed

docs/index.html

+34-7
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,14 @@
8484
display: flex;
8585
flex-direction: column;
8686
align-items: center;
87-
top: 80px;
87+
justify-content: center;
88+
max-width: 100%;
8889
}
8990
h1 {
9091
color: black;
9192
font-size: 80px;
9293
line-height: 1;
93-
margin-bottom: 32px;
94+
margin-bottom: 16px;
9495
}
9596
@media screen and (min-width: 768px) {
9697
h1 {
@@ -111,17 +112,25 @@
111112
display: flex;
112113
flex-direction: column;
113114
gap: 12px;
115+
max-width: 100%;
114116
}
115117
code {
116118
display: block;
117119
font-family: "Courier New", Courier, monospace;
118120
font-size: 14px;
119121
}
120122

121-
#releastBalloonsButton {
123+
#releaseBalloonsButton {
122124
width: 100%;
123125
}
124126

127+
.links {
128+
display: flex;
129+
gap: 16px;
130+
align-items: center;
131+
justify-content: center;
132+
margin-bottom: 16px;
133+
}
125134
a {
126135
text-align: center;
127136
color: blue;
@@ -133,13 +142,31 @@
133142
<body>
134143
<div class="container">
135144
<h1>Balloons</h1>
145+
<div class="links">
146+
<span
147+
>Star on
148+
<a href="https://github.com/arturbien/balloons-js">Github</a></span
149+
>
150+
<span></span>
151+
<span>Made by <a href="https://x.com/artur_bien">Artur Bień</a></span>
152+
</div>
153+
136154
<div class="code-wrapper">
137155
<pre><code>npm install balloons-js</code></pre>
138-
<pre><code>import { balloons } from "balloons-js";
156+
<pre><code>import { balloons, textBalloons } from "balloons-js";
139157

140-
balloons();</code></pre>
141-
<button id="releastBalloonsButton">Release Balloons</button>
142-
<a href="https://github.com/arturbien/balloons-js">Github</a>
158+
balloons();
159+
160+
textBalloons([
161+
{
162+
text: "💩🔥😈",
163+
fontSize: 120
164+
color: "#000000",
165+
},
166+
]);
167+
</code></pre>
168+
<button id="releaseBalloonsButton">Release Balloons</button>
169+
<button id="releaseTextBalloonsButton">Release Text Balloons</button>
143170
</div>
144171
</div>
145172
<script src="script.js"></script>

docs/script.js

+370-197
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/script.ts

+45-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,53 @@
11
import { balloons } from "../src/index";
2-
2+
import { textBalloons } from "../src/textBalloons";
33
document.addEventListener("DOMContentLoaded", () => {
44
balloons();
5-
const button = document.getElementById("releastBalloonsButton");
5+
const button = document.getElementById("releaseBalloonsButton");
66

77
button?.addEventListener("click", () => {
88
balloons();
99
});
10+
11+
const textButton = document.getElementById("releaseTextBalloonsButton");
12+
13+
function releaseTextBalloons() {
14+
const colors = [
15+
// lemon
16+
"#DBF505EE",
17+
// blue
18+
"#1754D8EE",
19+
// orange
20+
"#FA4616EE",
21+
// lime
22+
"#06D718EE",
23+
// magenta,
24+
"#FF008DEE",
25+
];
26+
// Pick first color randomly
27+
const firstColorIndex = Math.floor(Math.random() * colors.length);
28+
// Pick second color by selecting from remaining colors
29+
const remainingColors = colors.filter(
30+
(_, index) => index !== firstColorIndex
31+
);
32+
const secondColorIndex = Math.floor(Math.random() * remainingColors.length);
33+
34+
textBalloons([
35+
{
36+
color: colors[firstColorIndex],
37+
fontSize: Math.min(window.innerWidth / 5, 160), // Scale linearly with screen width up to 90px
38+
text: `HAPPY`,
39+
},
40+
{
41+
color: remainingColors[secondColorIndex],
42+
fontSize: Math.min(window.innerWidth / 4, 160), // Scale linearly with screen width up to 120px
43+
text: `BDAY`,
44+
},
45+
{
46+
text: "💩🔥😈",
47+
fontSize: Math.min(window.innerWidth / 4, 160), // Scale linearly with screen width up to 120px
48+
color: "#000000",
49+
},
50+
]);
51+
}
52+
textButton?.addEventListener("click", releaseTextBalloons);
1053
});

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "balloons-js",
3-
"version": "0.0.1",
3+
"version": "0.0.3",
44
"description": "Animated balloons effect for your website",
55
"main": "dist/index.js",
66
"module": "dist/index.esm.js",
@@ -11,7 +11,7 @@
1111
"scripts": {
1212
"build": "rollup -c",
1313
"dev": "rollup -c -w",
14-
"serve:docs": "npm run build && npx http-server docs -p 3000"
14+
"serve:docs": "npm run dev && npx http-server docs -p 3000"
1515
},
1616
"author": "Artur Bień <artur.bien@gmail.com>",
1717
"license": "MIT",

src/balloonFont.ts

+11
Large diffs are not rendered by default.

src/balloons.ts

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import {
2+
balloonDefaultSize,
3+
createBallonElement,
4+
svgFiltersHtml,
5+
} from "./balloonSvg";
6+
export { textBalloons } from "./textBalloons";
7+
8+
const easings = [
9+
// easeOutQuint
10+
"cubic-bezier(0.22, 1, 0.36, 1)",
11+
// easeOutCubic
12+
"cubic-bezier(0.33, 1, 0.68, 1)",
13+
];
14+
const colorPairs = [
15+
// yellow
16+
["#ffec37ee", "#f8b13dff"],
17+
// red
18+
["#f89640ee", "#c03940ff"],
19+
//blue
20+
["#3bc0f0ee", "#0075bcff"],
21+
// green
22+
["#b0cb47ee", "#3d954bff"],
23+
// purple
24+
["#cf85b8ee", "#a3509dff"],
25+
];
26+
27+
function createBalloonAnimation({
28+
balloon,
29+
x,
30+
y,
31+
z,
32+
targetX,
33+
targetY,
34+
targetZ,
35+
zIndex,
36+
}: {
37+
balloon: HTMLElement;
38+
x: number;
39+
y: number;
40+
z: number;
41+
targetX: number;
42+
targetY: number;
43+
targetZ: number;
44+
zIndex: number;
45+
}) {
46+
balloon.style.zIndex = zIndex.toString();
47+
// Add blur to the closes ballons for bokeh effect
48+
balloon.style.filter = `blur(${zIndex > 7 ? 8 : 0}px)`;
49+
const getAnimation = () => {
50+
const tiltAngle = Math.random() * (15 - 8) + 8; // Random tilt angle between 8 and 15 degrees
51+
const tiltDirection = Math.random() < 0.5 ? 1 : -1; // Random tilt direction
52+
return balloon.animate(
53+
[
54+
{
55+
transform: `translate(-50%, 0%) translate3d(${x}px, ${y}px, ${z}px) rotate3d(0, 0, 1, ${
56+
tiltDirection * -tiltAngle
57+
}deg)`,
58+
opacity: 1,
59+
},
60+
{
61+
transform: `translate(-50%, 0%) translate3d(${
62+
x + (targetX - x) / 2
63+
}px, ${y + (y + targetY * 5 - y) / 2}px, ${
64+
z + (targetZ - z) / 2
65+
}px) rotate3d(0, 0, 1, ${tiltDirection * tiltAngle}deg)`,
66+
opacity: 1,
67+
offset: 0.5,
68+
},
69+
{
70+
transform: `translate(-50%, 0%) translate3d(${targetX}px, ${
71+
y + targetY * 5
72+
}px, ${targetZ}px) rotate3d(0, 0, 1, ${
73+
tiltDirection * -tiltAngle
74+
}deg)`,
75+
opacity: 1,
76+
},
77+
],
78+
{
79+
duration: (Math.random() * 1000 + 5000) * 5,
80+
easing: easings[Math.floor(Math.random() * easings.length)],
81+
delay: zIndex * 200,
82+
}
83+
);
84+
};
85+
return { balloon, getAnimation };
86+
}
87+
88+
export function balloons(): Promise<void> {
89+
return new Promise((resolve) => {
90+
const balloonsContainer = document.createElement("balloons");
91+
92+
Object.assign(balloonsContainer.style, {
93+
overflow: "hidden",
94+
position: "fixed",
95+
inset: "0",
96+
zIndex: "999",
97+
display: "inline-block",
98+
pointerEvents: "none",
99+
perspective: "1500px",
100+
perspectiveOrigin: "50vw 100vh",
101+
contain: "style, layout, paint",
102+
});
103+
104+
document.documentElement.appendChild(balloonsContainer);
105+
106+
const sceneSize = { width: window.innerWidth, height: window.innerHeight };
107+
// make balloon height relative to screen size for this nice bokeh/perspective effect
108+
const balloonHeight = Math.floor(
109+
Math.min(sceneSize.width, sceneSize.height) * 1
110+
);
111+
112+
const balloonWidth =
113+
(balloonDefaultSize.width / balloonDefaultSize.height) * balloonHeight;
114+
let amount = Math.max(
115+
7,
116+
Math.round(window.innerWidth / (balloonWidth / 2))
117+
);
118+
// make max dist depend on number of balloons and their size for realistic effect
119+
// we dont want them to be too separated or too squeezed together
120+
const maxDist = Math.max(
121+
(amount * balloonWidth) / 2,
122+
(balloonWidth / 2) * 10
123+
);
124+
125+
type BallonPosition = {
126+
x: number;
127+
y: number;
128+
z: number;
129+
targetX: number;
130+
targetY: number;
131+
targetZ: number;
132+
};
133+
134+
let balloonPositions: BallonPosition[] = [];
135+
136+
for (let i = 0; i < amount; i++) {
137+
const x = Math.round(sceneSize.width * Math.random());
138+
// make sure balloons first render below the bottom of the screen
139+
const y = window.innerHeight;
140+
const z = Math.round(-1 * (Math.random() * maxDist));
141+
142+
const targetX = Math.round(
143+
x + Math.random() * balloonWidth * 6 * (Math.random() > 0.5 ? 1 : -1)
144+
);
145+
const targetY = -window.innerHeight;
146+
// balloons don't move in the Z direction
147+
const targetZ = z;
148+
balloonPositions.push({
149+
x,
150+
y,
151+
z,
152+
targetX,
153+
targetY,
154+
targetZ,
155+
});
156+
}
157+
158+
balloonPositions = balloonPositions.sort((a, b) => a.z - b.z);
159+
const closestBallonPosition = balloonPositions[balloonPositions.length - 1];
160+
const farthestBallonPosition = balloonPositions[0];
161+
// console.log({ closestBallonPosition, farthestBallonPosition });
162+
balloonPositions = balloonPositions.map((pos) => ({
163+
...pos,
164+
z: pos.z - closestBallonPosition.z,
165+
targetZ: pos.z - closestBallonPosition.z,
166+
}));
167+
168+
const filtersElement = document.createElement("div");
169+
filtersElement.innerHTML = svgFiltersHtml;
170+
balloonsContainer.appendChild(filtersElement);
171+
172+
let currentZIndex = 1;
173+
174+
const animations = balloonPositions.map((pos, index) => {
175+
const colorPair = colorPairs[index % colorPairs.length];
176+
177+
const balloon = createBallonElement({
178+
balloonColor: colorPair[1],
179+
lightColor: colorPair[0],
180+
width: balloonWidth,
181+
});
182+
balloonsContainer.appendChild(balloon);
183+
184+
return createBalloonAnimation({
185+
balloon,
186+
...pos,
187+
zIndex: currentZIndex++,
188+
});
189+
});
190+
191+
// Wait a bit for the balloon prerender
192+
requestAnimationFrame(() => {
193+
const animationPromises = animations.map(({ balloon, getAnimation }) => {
194+
const a = getAnimation();
195+
return a.finished.then(() => {
196+
balloon.remove();
197+
});
198+
});
199+
200+
Promise.all(animationPromises).then(() => {
201+
balloonsContainer.remove();
202+
resolve();
203+
});
204+
});
205+
});
206+
}

0 commit comments

Comments
 (0)