BLOG
泡のアニメーション3種まとめ

INDEX
今回、3つの泡のアニメーションを requestAnimationFrame を使って作ってみました。
それぞれ違う飲み物をイメージして、泡の動きや見た目を変えています。
どれも基本的には「泡のDOMを生成 → 上昇アニメーションを実行」という流れになっていますが、 それぞれの演出には少しずつ工夫があります。
今回は、その工夫したポイントを1つずつ紹介していきます。
1. コーラの泡
コーラは元気なイメージなので、泡も大きめにして、動きも少し早くしました。
コードでは、泡を setInterval でたくさん作り、それぞれ requestAnimationFrame で上に上がるようにしています。
コード:
const foamContainer = document.getElementById(`foam-container`);
let foams = [];
let clientWidth = document.documentElement.clientWidth;
let clientHeight = document.documentElement.clientHeight;
window.addEventListener('resize', () => {
clientWidth = document.documentElement.clientWidth;
clientHeight = document.documentElement.clientHeight;
});
function cokeFoam (){
const positionX = Math.floor(Math.random() * (clientWidth - 50)) +20;
const positionY = Math.floor(Math.random() * (clientHeight));
const size = Math.floor(Math.random() * 21 ) + 5;
const foam = document.createElement('div');
foam.className = 'bubble';
foam.style.width = `${size}px`;
foam.style.height = foam.style.width;
foam.style.left = `${positionX}px`;
foam.style.bottom = `${positionY}px`;
foamContainer.appendChild(foam);
foams.push({ el: foam, y: positionY});
}
function foamBooster () {
const speed = Math.floor(Math.random() * 10 ) +4;
foams.forEach((f, i) => {
f.y += speed;
f.el.style.transform = `translateY(-${f.y}px)`;
if(f.y > clientHeight){
f.el.remove();
foams.splice(i, 1);
}
})
requestAnimationFrame(foamBooster);
}
requestAnimationFrame(foamBooster);
setInterval(cokeFoam, 10);
動き自体はシンプルで、translateYを使って上に動かしています。
ランダムな位置とサイズで出てくるので、炭酸っぽい印象になります。
2. ビールの泡
ビールの泡は、上にあがったあと、泡の層としてしばらく残る印象があります。
そこで、泡が document.documentElement.clientHeightの上部に到達したら、bubble-top というクラスを追加して、その場にとどまっている泡のように見せています。
foam.classList.add('bubble-top');
このクラスでは、opacity や background-color を少し明るくして、「泡が溜まっている感」を出すようにしています。
.bubble-top {
box-shadow: 0 0 4px rgba(255, 255, 255, 0.2);
background-color: rgba(255, 255, 255, 0.8);
transition: opacity 0.5s ease;
}
さらに、requestAnimationFrame の time を使って deltaTime を計算し、アニメーションを時間に合わせて制御するようにしました。泡が上まで行ったら数秒とどまり、そのあと自然に opacity が下がって消えます。
const randomTime = (Math.floor(Math.random() * 3) + 1) * 1000;
setTimeout(() => {
f.el.style.opacity = 0;
setTimeout(() => f.el.remove(), 500);
}, randomTime);
結果的に、「上に溜まっていく白い泡の層」ができて、ビールらしい見た目に仕上がりました。
コード:
const foamContainer = document.getElementById(`foam-container`);
const bubbleTops = document.querySelectorAll(`.bubble-top`);
const topBuffer = 100;
let foams = [];
let lastTime = null;
let clientWidth = document.documentElement.clientWidth;
let clientHeight = document.documentElement.clientHeight;
window.addEventListener('resize', () => {
clientWidth = document.documentElement.clientWidth;
clientHeight = document.documentElement.clientHeight;
});
function topFoam () {
const size = Math.floor(Math.random() * 15 ) + 5;
const topY = clientHeight - topBuffer + 3;
bubbleTops.forEach(bubbleTop => {
const size = Math.floor(Math.random() * 15 ) + 5;
const topX = Math.floor(Math.random() * (clientWidth - 30)) +20;
bubbleTop.style.width = `${size}px`;
bubbleTop.style.height = bubbleTop.style.width;
bubbleTop.style.left = `${topX}px`;
bubbleTop.style.bottom = `${topY}px`;
})
}
setInterval(topFoam,200);
function beerFoam (){
const positionX = Math.floor(Math.random() * (clientWidth - 30)) +20;
const positionY = Math.floor(Math.random() * (clientHeight));
const size = Math.floor(Math.random() * 15 ) + 5;
const foam = document.createElement('div');
foam.className = 'bubble';
foam.style.width = `${size}px`;
foam.style.height = foam.style.width;
foam.style.left = `${positionX}px`;
foam.style.bottom = `${positionY}px`;
foamContainer.appendChild(foam);
foams.push({ el: foam, y: positionY});
}
setInterval(beerFoam,30);
function foamBooster (time) {
if (!lastTime) lastTime = time;
const delta = time - lastTime;
lastTime = time;
const randomTime = (Math.floor(Math.random() * 3) + 1) * 1000;
foams.forEach((f, i) => {
f.y += delta * 0.1;
f.el.style.bottom = `${f.y}px`;
if(f.y >= clientHeight - topBuffer){
f.el.style.bottom = `${clientHeight - topBuffer + 3}px`;
f.el.classList.add('bubble-top');
setTimeout(() => {
f.el.style.opacity = 0;
setTimeout(() => f.el.remove(), 500);
}, randomTime);
foams.splice(i, 1);
}
})
requestAnimationFrame(foamBooster);
}
requestAnimationFrame(foamBooster);
3. シャンパンの泡
シャンパンの泡は、小さくて、細くて、上にまっすぐ上がっていくイメージです。
そのため、requestAnimationFrame を使って全ての泡を管理し、位置を一括で更新しています。
また、ChampagneFoam() でランダムな位置に泡を出しつつ、決まった位置にも泡を出すことで、「ランダムさ」と「直線的な泡」を両立させました。
コード:
const foamContainer = document.getElementById('foam-container');
let foams = [];
let clientWidth = document.documentElement.clientWidth;
let clientHeight = document.documentElement.clientHeight;
window.addEventListener('resize', () => {
clientWidth = document.documentElement.clientWidth;
clientHeight = document.documentElement.clientHeight;
});
function createBubbleAt(x, startY) {
const foam = document.createElement('div');
foam.className = 'bubble';
const speed = Math.floor(Math.random() * 10 ) +4;
const size = Math.random() * 3 + 2;
foam.style.width = `${size}px`;
foam.style.height = foam.style.width;
foam.style.left = `${x}px`;
foam.style.bottom = `${startY}px`;
foam.style.opacity = 1;
foamContainer.appendChild(foam);
foams.push({ el: foam, y: startY, speed});
}
function ChampagneFoam (){
createBubbleAt(Math.random() * (clientWidth - 100) + 50, Math.random() * (clientHeight - 100) + 50);
createBubbleAt(Math.random() * (clientWidth - 100) + 50, Math.random() * (clientHeight - 100) + 50);
createBubbleAt((3 * clientWidth) / 4, 0);
createBubbleAt(clientWidth / 4, 200);
createBubbleAt(clientWidth / 5, 50);
createBubbleAt(clientWidth / 2, 150);
createBubbleAt(clientWidth * 2 / 3, 80);
}
function foamBooster() {
foams.forEach((f, i) => {
f.y += f.speed;
f.el.style.bottom = `${f.y}px`;
if (f.y < clientHeight - 100) {
f.el.style.opacity = `${1 - (f.y - (clientHeight - 100)) / 100}`;
}
if (f.y >= clientHeight) {
f.el.remove();
foams.splice(i, 1);
}
})
requestAnimationFrame(foamBooster);
}
requestAnimationFrame(foamBooster);
setInterval(ChampagneFoam, 200);
まとめ
3つとも requestAnimationFrame を使っていますが、それぞれ使い方が少しずつ違います:
飲み物 | 特徴 | 技術ポイント |
---|---|---|
コーラ | 大きくて速く、たくさん出る | cokeFoam() で泡を作り、foamBooster() でまとめて動かす。translateY でシンプルに上昇。 |
ビール | 上にたまって、少しして消える | beerFoam() で泡を生成。deltaTime で動きを調整し、上に着いたら setTimeout() で自然にフェードアウト。 |
シャンパン | 小さくて細かい、いろんな場所から出る | ChampagneFoam() で泡を出す位置を決める。foamBooster() で一括管理し、opacity で自然に消えるようにした。 |
泡の動きには飲み物のキャラクターが出るので、見た目の調整もとても楽しかったです。
それぞれの特徴にあわせてアニメーションを変えることで、もっと自然でリアルな表現ができました。
参考記事
https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval
https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame
https://techplay.jp/column/548
https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
https://ics.media/entry/210414/
https://www.javadrive.jp/javascript/event/index9.html#google_vignette