#app(
@mousemove="chargeFollow",
@mouseover="chargeShow",
@mouseleave="chargeHide",
@mousedown="chargeBubbles",
@mouseup="releaseBubbles",
@touchcancel="chargeHideTouch",
@touchmove="chargeFollowTouch",
@touchstart="chargeBubblesTouch",
@touchend="releaseBubblesTouch"
)
.text
h1(ref="textTitle") BUBBLE LAUNCHER
p(ref="textDirection") Hold and click the screen to launch bubbles.
p(ref="textDescription") There are 4 steps in releasing bubbles.
.bubble(v-for="bubble, i in bubbles"
:class="{" +
"'popping' : bubble.popping," +
"'popped' : bubble.popped" +
"}"
:ref="'bubble' + i"
v-if="bubble !== null")
transition(name="charge")
.charge(
:class="{" +
" 'step-1' : step === 1" +
", 'step-2' : step === 2" +
", 'step-3' : step === 3" +
", 'step-4' : step === 4" +
"}",
:style="{ top: Charge.Y, left: Charge.X }",
@mouseout="",
ref="charge",
v-if="show === true"
)
svg(viewBox="0 0 100 100" ref="chargeImage")
circle(r="45", cx="50", cy="50")
View Compiled
$pi: 3.14159;
$radius: 45;
$circumference: 2 * $pi * $radius;
@import url("https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap");
#app {
background-image: linear-gradient(to bottom right, #5eb7ff, #428eff, #003dc2);
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}
.text {
color: #ffffff;
font-size: 180%;
text-align: center;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
user-select: none;
h1,
p {
font-family: "Fredoka One", cursive;
padding: 0;
margin: 10px 25px;
filter: blur(1px);
opacity: 0.7;
text-shadow: 0 -1px #000000;
}
}
.bubble {
background-color: rgba(#ffffff, 0.3);
width: 50px;
height: 50px;
border: solid 4px rgba(#ffffff, 0.5);
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
border-radius: 50%;
filter: blur(1px);
&:after {
content: "";
background-color: rgba(#ffffff, 0.7);
width: 20px;
height: 10px;
position: absolute;
top: 7px;
left: 3px;
border-radius: 50% 50% 50% 50% / 75% 75% 50% 50%;
transform: rotate(-35deg);
filter: blur(3px);
}
&:not(.popping) {
animation: bubble 300ms infinite;
}
}
.charge {
stroke-dasharray: 0 $circumference;
background-color: rgba(#303030, 0.5);
width: 50px;
height: 50px;
position: absolute;
border-radius: 50%;
box-shadow: 0 0 0 0 rgba(#f23030, 0.5);
transition: top 300ms ease-out, left 300ms ease-out,
background-color 300ms ease-out, box-shadow 300ms ease-out;
&.step-1 {
animation: step-small 300ms infinite;
}
&.step-2 {
animation: step-small 150ms infinite;
}
&.step-3 {
animation: step-large 120ms infinite;
}
&.step-4 {
background-color: rgba(#f23030, 0.7);
box-shadow: 0 0 12px 8px rgba(#f23030, 0.7);
animation: step-large 90ms infinite;
svg {
stroke: transparent;
}
}
svg {
fill: transparent;
stroke: rgba(#ffffff, 0.7);
stroke-width: 8;
stroke-dasharray: inherit;
width: calc(100% + 8px);
height: calc(100% + 8px);
margin-top: -4px;
margin-left: -4px;
transform: rotate(-90deg);
transition: stroke 300ms ease-out;
}
}
.charge-enter-active {
transition: opacity 150ms ease-out;
animation: show 300ms forwards;
}
.charge-leave-active {
transition: opacity 150ms ease-out;
}
.charge-enter,
.charge-leave-to {
opacity: 0;
}
@keyframes show {
0% {
transform: scale(0);
}
40% {
transform: scale(1.25);
}
60% {
transform: scale(0.8);
}
80% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes step-small {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes step-large {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
@keyframes bubble {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-1px);
}
50% {
transform: translateX(0);
}
75% {
transform: translateX(1px);
}
100% {
transform: translateX(0);
}
}
View Compiled
const Circumference = 2 * Math.PI * 45;
const DistanceFrom = 200;
const DistanceTo = 400;
const DelayTime = 30;
let Charge = {
value: 0
};
function randomDistance() {
return random(DistanceFrom, DistanceTo);
}
function random(from, to) {
return Math.random() * (to - from) + from;
}
function animateBubbles() {
for (let i = 0; i < app.bubbles.length; i++) {
let bubble = app.bubbles[i];
if (bubble === null) {
continue;
}
if (bubble.isAnimating === true) {
continue;
}
let bubbleElm = app.$refs["bubble" + i];
if (typeof bubbleElm === "undefined") {
continue;
}
gsap.set(bubbleElm, { top: bubble.y, left: bubble.x });
gsap.to(bubbleElm, {
duration: bubble.duration,
top:
bubble.y - Math.sin((bubble.angle * Math.PI) / 180) * randomDistance(),
left:
bubble.x + Math.cos((bubble.angle * Math.PI) / 180) * randomDistance(),
ease: Power2.easeOut,
onComplete() {
app.bubbles[i].popping = true;
gsap.to(bubbleElm, {
duration: 1,
css: { scale: 2, opacity: 0, filter: "blur(8px)" },
onComplete() {
app.bubbles[i].popped = true;
try {
app.bubbles[i] = null;
} catch (ex) {
// Do nothing
}
}
});
}
});
app.bubbles[i].isAnimating = true;
}
}
function createBubble(a, d, x, y) {
app.bubbles.push({
angle: a,
duration: d,
x: x - 25,
y: y - 25,
popping: false,
popped: false,
isAnimating: false
});
}
function bubbles1(x, y) {
createBubble(90, random(8, 10), x, y);
}
function bubbles2(x, y, i, j) {
setTimeout(function() {
if (i === j) {
return;
}
createBubble(90 + random(-45, 45), random(6, 8), x, y);
if (i < j) {
bubbles2(x, y, ++i, j);
}
}, random(0, DelayTime));
}
function bubbles3(x, y, i, j) {
setTimeout(function() {
if (i === j) {
return;
}
createBubble(90 + random(-60, 60), random(4, 6), x, y);
if (i < j) {
bubbles3(x, y, ++i, j);
}
}, random(0, DelayTime));
}
function bubbles4(x, y, i, j) {
setTimeout(function() {
if (i === j) {
return;
}
createBubble(random(0, 360), random(1, 4), x, y);
if (i < j) {
bubbles4(x, y, ++i, j);
}
}, random(0, DelayTime));
}
let app = new Vue({
el: "#app",
data: {
show: false,
Charge: {
X: 0,
Y: 0
},
step: 0,
bubbles: []
},
methods: {
chargeFollow(evt) {
this.Charge.X = evt.clientX - 25 + "px";
this.Charge.Y = evt.clientY - 25 + "px";
},
chargeShow() {
this.show = true;
},
chargeHide() {
this.show = false;
},
chargeBubbles() {
let self = this;
let circ = Circumference;
Charge.value = 0;
gsap.killTweensOf(Charge);
gsap.to(Charge, 10, {
value: 100,
ease: Power1.easeOut,
onUpdate() {
let charge = Charge.value;
gsap.to(self.$refs.charge, 0.1, {
strokeDasharray: Math.floor(circ * (charge / 100)) + " " + circ,
ease: Power2.easeOut
});
if (Math.floor(charge) > 0 && Math.floor(charge) < 33) {
self.step = 1;
} else if (Math.floor(charge) >= 33 && Math.floor(charge) < 66) {
self.step = 2;
} else if (Math.floor(charge) >= 66 && Math.floor(charge) < 100) {
self.step = 3;
} else if (Math.floor(charge) >= 100) {
self.step = 4;
}
}
});
},
releaseBubbles(evt) {
this.shootBubbles(evt.clientX, evt.clientY);
},
shootBubbles(x, y) {
gsap.killTweensOf(Charge);
gsap.to(this.$refs.charge, 0.2, {
strokeDasharray: "0 " + Circumference
});
switch (this.step) {
case 1:
bubbles1(x, y);
break;
case 2:
bubbles2(x, y, 0, random(4, 8));
break;
case 3:
bubbles3(x, y, 0, random(8, 16));
break;
case 4:
bubbles4(x, y, 0, random(32, 64));
break;
}
animateBubbles();
this.step = 0;
},
chargeFollowTouch(evt) {
if (this.show === true) {
this.Charge.X = evt.touches[0].clientX - 25 + "px";
this.Charge.Y = evt.touches[0].clientY - 25 + "px";
}
},
chargeHideTouch() {
this.show = false;
},
chargeBubblesTouch(evt) {
this.show = true;
this.Charge.X = evt.touches[0].clientX - 25 + "px";
this.Charge.Y = evt.touches[0].clientY - 25 + "px";
this.chargeBubbles();
},
releaseBubblesTouch() {
this.shootBubbles(
parseInt(this.Charge.X.replace(/px/, "")),
parseInt(this.Charge.Y.replace(/px/, ""))
);
this.chargeHideTouch();
}
},
mounted() {
let textTitle = this.$refs.textTitle;
let textDirection = this.$refs.textDirection;
let textDescription = this.$refs.textDescription;
let titleSplit = new SplitText(textTitle);
let directionSplit = new SplitText(textDirection);
let descriptionSplit = new SplitText(textDescription);
gsap.from(titleSplit.chars, {
duration: 2,
y: 100,
autoAlpha: 0,
ease: "elastic",
stagger: 0.08
});
gsap.from(directionSplit.chars, {
duration: 3,
y: 50,
autoAlpha: 0,
ease: "elastic",
stagger: 0.08
});
gsap.from(descriptionSplit.chars, {
duration: 3,
y: 50,
autoAlpha: 0,
ease: "elastic",
stagger: 0.08
});
},
updated() {
animateBubbles();
}
});
View Compiled
This Pen doesn't use any external CSS resources.