コードエディター 本文
<div class="wrapper">
<div id="stage">
<div id="shadow"></div>
<div id="ball"></div>
</div>
<button id="floatButton">PUSH TO FLOAT</button>
</div>
カスタムJavaScript
const ball = document.getElementById('ball');
const shadow = document.getElementById('shadow');
const stage = document.getElementById('stage');
const btn = document.getElementById('floatButton');
let yPos = 0;
let vY = 0;
let time = 0;
let isPressing = false;
const gravity = 0.25;
const lift = 0.7;
const friction = 0.96;
function createBubble() {
const bubble = document.createElement('div');
bubble.className = 'bubble';
const size = Math.random() * 6 + 3 + 'px'; // 少し小さめに
bubble.style.width = size;
bubble.style.height = size;
// 320pxの幅に合わせて発生範囲を調整
bubble.style.left = (stage.offsetWidth / 2 + (Math.random() - 0.5) * 30) + 'px';
bubble.style.bottom = (yPos + 30) + 'px';
stage.appendChild(bubble);
let bY = yPos + 30;
let bX = 0;
function animateBubble() {
bY += 2.5;
bX += Math.sin(bY * 0.06);
bubble.style.transform = `translate(${bX}px, ${-bY + yPos + 30}px)`;
bubble.style.opacity = 1 - (bY / 400);
if (bY < 400) {
requestAnimationFrame(animateBubble);
} else {
bubble.remove();
}
}
animateBubble();
}
const startFloating = () => isPressing = true;
const stopFloating = () => isPressing = false;
btn.addEventListener('mousedown', startFloating);
btn.addEventListener('mouseup', stopFloating);
btn.addEventListener('mouseleave', stopFloating);
btn.addEventListener('touchstart', (e) => { e.preventDefault(); startFloating(); });
btn.addEventListener('touchend', stopFloating);
function update() {
time += 0.04;
if (isPressing) {
vY += lift;
if (Math.random() > 0.85) createBubble();
} else {
vY -= gravity;
}
vY *= friction;
yPos += vY;
if (yPos < 0) { yPos = 0; vY *= -0.3; }
// ボールのサイズ(50px)に合わせて天井を調整
if (yPos > 295) { yPos = 295; vY *= -0.3; }
// 左右のゆらぎ幅を320pxに合わせて少し抑えめ(10px)に設定
let driftX = Math.sin(time * 0.8) * 10;
let driftY = Math.sin(time * 1.2) * 4;
let stretch = 1 + Math.abs(vY) * 0.015;
let squash = 1 / stretch;
ball.style.transform = `translate(calc(-50% + ${driftX}px), ${-yPos - driftY}px) scale(${squash}, ${stretch})`;
let shadowOpacity = Math.max(0.1, 0.4 - (yPos / 400));
let shadowScale = 1 + (yPos / 250);
shadow.style.opacity = shadowOpacity;
shadow.style.transform = `translateX(-50%) scale(${shadowScale})`;
requestAnimationFrame(update);
}
update();
カスタムCSS
.wrapper {
display: flex;
flex-direction: column;
align-items: center; /* 中のボタンを中央寄せ */
width: 320px;
}
#stage {
width: 320px;
height: 400px;
background: linear-gradient(to bottom, #4fc3f7, #0288d1);
position: relative;
overflow: hidden;
border-radius: 15px;
border: 6px solid #fff;
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
margin-bottom: 20px;
}
#shadow {
position: absolute; bottom: 20px; left: 50%;
width: 50px; height: 12px; background: rgba(0, 0, 0, 0.3);
border-radius: 50%; transform: translateX(-50%); filter: blur(3px);
}
#ball {
width: 50px; height: 50px;
background: radial-gradient(circle at 30% 30%, #ff8a65, #e64a19);
border-radius: 50%; position: absolute; bottom: 30px; left: 50%; z-index: 10;
}
.bubble {
position: absolute; background: rgba(255, 255, 255, 0.4);
border: 1px solid rgba(255, 255, 255, 0.6); border-radius: 50%; pointer-events: none;
}
button {
padding: 12px 24px; font-size: 16px; font-weight: bold; color: white;
background: #00796b; border: none; border-radius: 50px; cursor: pointer;
box-shadow: 0 4px #004d40; user-select: none;
transition: all 0.1s;
}
button:active { transform: translateY(2px); box-shadow: 0 2px #004d40; }
