<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Runaway Button</title>
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
font-family: 'Fredoka One', cursive;
overflow: hidden;
}
.scene {
position: relative;
width: 90vw;
max-width: 800px;
height: 80vh;
max-height: 600px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.btn-wrapper {
position: relative;
width: 180px;
height: 50px;
}
.runaway-btn {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(30px) saturate(1.8);
-webkit-backdrop-filter: blur(30px) saturate(1.8);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 25px;
color: rgba(255, 255, 255, 0.95);
font-family: 'Fredoka One', cursive;
font-size: 20px;
font-weight: 400;
letter-spacing: 2px;
cursor: pointer;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -1px 0 rgba(255, 255, 255, 0.05);
z-index: 10;
user-select: none;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
text-transform: lowercase;
opacity: 1;
transform: scale(1);
transition: opacity 0.2s, transform 0.2s, backdrop-filter 0.1s;
will-change: transform, opacity;
}
.runaway-btn::before {
content: '';
position: absolute;
top: -50%; left: -50%;
width: 200%; height: 200%;
background: radial-gradient(
ellipse at 30% 20%,
rgba(255, 255, 255, 0.4) 0%,
rgba(255, 255, 255, 0.1) 40%,
transparent 70%
);
pointer-events: none;
}
.runaway-btn::after {
content: '';
position: absolute;
bottom: 0; left: 0; right: 0;
height: 40%;
background: linear-gradient(to top, rgba(255, 255, 255, 0.08) 0%, transparent 100%);
pointer-events: none;
border-radius: 0 0 25px 25px;
}
.runaway-btn.scattering {
opacity: 0;
transform: scale(0.5);
pointer-events: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
will-change: transform, opacity;
}
.runaway-btn.condensing {
animation: condense-in 0.25s ease-out forwards;
}
@keyframes condense-in {
0% { opacity: 0; transform: scale(1.5); filter: blur(10px); }
50% { opacity: 0.7; transform: scale(0.9); filter: blur(3px); }
100% { opacity: 1; transform: scale(1); filter: blur(0); }
}
.runaway-btn.shake {
animation: btn-shake 0.15s ease-in-out 3;
}
@keyframes btn-shake {
0%, 100% { margin-left: 0; margin-top: 0; }
25% { margin-left: -3px; margin-top: -2px; }
50% { margin-left: 3px; margin-top: 2px; }
75% { margin-left: -2px; margin-top: 3px; }
}
.leg {
position: absolute;
width: 5px; height: 0;
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(15px) saturate(1.5);
-webkit-backdrop-filter: blur(15px) saturate(1.5);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 3px;
transform-origin: top center;
transition: height 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
opacity: 0;
z-index: 5;
pointer-events: none;
}
.leg::after {
content: '';
position: absolute;
bottom: -3px; left: 50%;
transform: translateX(-50%);
width: 9px; height: 5px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.leg.front-left { top: 14px; left: 25px; transform: rotate(-20deg); }
.leg.front-right { top: 14px; right: 25px; transform: rotate(20deg); }
.leg.back-left { bottom: 14px; left: 30px; transform: rotate(20deg); }
.leg.back-right { bottom: 14px; right: 30px; transform: rotate(-20deg); }
.btn-wrapper.running .leg { height: 22px; opacity: 1; }
.btn-wrapper.running .leg.front-left { animation: leg-move-1 0.3s infinite alternate; }
.btn-wrapper.running .leg.front-right { animation: leg-move-2 0.3s infinite alternate; }
.btn-wrapper.running .leg.back-left { animation: leg-move-2 0.3s infinite alternate; }
.btn-wrapper.running .leg.back-right { animation: leg-move-1 0.3s infinite alternate; }
@keyframes leg-move-1 {
0% { transform: rotate(-20deg) translateY(0); }
100% { transform: rotate(-40deg) translateY(-5px); }
}
@keyframes leg-move-2 {
0% { transform: rotate(20deg) translateY(0); }
100% { transform: rotate(40deg) translateY(-5px); }
}
.shadow {
position: absolute;
bottom: -15px; left: 50%;
transform: translateX(-50%);
width: 140px; height: 15px;
background: rgba(0,0,0,0.5);
border-radius: 50%;
filter: blur(8px);
pointer-events: none;
opacity: 1;
transition: opacity 0.2s;
}
.shadow.hidden {
opacity: 0;
}
.chat-bubble {
position: absolute;
top: -55px;
padding: 10px 20px;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
color: rgba(255, 255, 255, 0.9);
font-family: 'Fredoka One', cursive;
font-size: 16px;
letter-spacing: 1px;
opacity: 0;
transform: scale(0.5) translateY(10px);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none;
z-index: 20;
white-space: nowrap;
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.chat-bubble.pos-right {
right: -20px;
border-bottom-right-radius: 4px;
}
.chat-bubble.pos-right::before {
content: '';
position: absolute;
bottom: -6px; right: 15px;
width: 12px; height: 12px;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
border-right: 1px solid rgba(255, 255, 255, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
transform: rotate(45deg);
}
.chat-bubble.pos-left {
left: -20px;
border-bottom-left-radius: 4px;
}
.chat-bubble.pos-left::before {
content: '';
position: absolute;
bottom: -6px; left: 15px;
width: 12px; height: 12px;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
border-left: 1px solid rgba(255, 255, 255, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
transform: rotate(45deg);
}
.chat-bubble.show { opacity: 1; transform: scale(1) translateY(0); }
.chat-bubble.hide { opacity: 0; transform: scale(0.8) translateY(-10px); }
.particle {
position: fixed;
border-radius: 50%;
pointer-events: none;
z-index: 15;
box-shadow: 0 0 4px 1px currentColor;
}
.particle-trail {
position: fixed;
height: 2px;
border-radius: 1px;
pointer-events: none;
z-index: 14;
opacity: 0.6;
filter: blur(1px);
}
.spark {
position: fixed;
border-radius: 50%;
pointer-events: none;
z-index: 16;
box-shadow: 0 0 5px 1.5px currentColor;
}
</style>
</head>
<body>
<div class="scene" id="scene">
<div class="btn-wrapper" id="btnWrapper">
<button class="runaway-btn" id="runawayBtn">button</button>
<div class="leg front-left"></div>
<div class="leg front-right"></div>
<div class="leg back-left"></div>
<div class="leg back-right"></div>
<div class="chat-bubble pos-right" id="chatBubble"></div>
</div>
<div class="shadow" id="shadow"></div>
</div>
<script>
const btnWrapper = document.getElementById('btnWrapper');
const btn = document.getElementById('runawayBtn');
const scene = document.getElementById('scene');
const chatBubble = document.getElementById('chatBubble');
const shadow = document.getElementById('shadow');
const bubbleTexts = ['itchy', 'ouch', 'hey', 'stop', 'no', 'yikes', 'ahh', 'eek'];
let currentX = 0, currentY = 0;
let clickCount = 0;
let isTeleporting = false;
let bubbleTimer = null;
let shakeTimer = null;
function getBounds() {
const wrapperRect = btnWrapper.getBoundingClientRect();
const sceneRect = scene.getBoundingClientRect();
const padding = 60;
return {
maxX: (sceneRect.width - wrapperRect.width) / 2 - padding,
maxY: (sceneRect.height - wrapperRect.height) / 2 - padding,
sceneWidth: sceneRect.width,
sceneHeight: sceneRect.height,
wrapperWidth: wrapperRect.width,
wrapperHeight: wrapperRect.height
};
}
function getRandomTarget() {
const { maxX, maxY } = getBounds();
const angle = Math.random() * Math.PI * 2;
const minDistance = 150;
const maxDistance = Math.min(maxX, maxY) * 0.7;
const distance = minDistance + Math.random() * (maxDistance - minDistance);
let targetX = currentX + Math.cos(angle) * distance;
let targetY = currentY + Math.sin(angle) * distance;
return {
x: Math.max(-maxX, Math.min(maxX, targetX)),
y: Math.max(-maxY, Math.min(maxY, targetY))
};
}
function updateBubblePosition() {
const { sceneWidth, wrapperWidth } = getBounds();
const bubbleWidth = 100;
const relativeX = currentX;
const rightEdge = sceneWidth / 2 + relativeX + wrapperWidth / 2 + bubbleWidth;
if (rightEdge > sceneWidth - 20) {
chatBubble.classList.remove('pos-right');
chatBubble.classList.add('pos-left');
} else {
chatBubble.classList.remove('pos-left');
chatBubble.classList.add('pos-right');
}
}
function createParticles(startX, startY, count, isCondense) {
const particles = [];
const colors = [
'rgba(255, 255, 255, 0.9)',
'rgba(200, 230, 255, 0.8)',
'rgba(255, 240, 200, 0.7)',
'rgba(180, 220, 255, 0.6)',
'rgba(220, 200, 255, 0.5)'
];
for (let i = 0; i < count; i++) {
const particle = document.createElement('div');
particle.className = isCondense ? 'spark' : 'particle';
const size = isCondense ? 0.5 + Math.random() * 2 : 1 + Math.random() * 3;
const color = colors[Math.floor(Math.random() * colors.length)];
const delay = Math.random() * 0.5;
particle.style.width = size + 'px';
particle.style.height = size + 'px';
particle.style.background = color;
particle.style.color = color;
particle.style.left = (startX + 90) + 'px';
particle.style.top = (startY + 25) + 'px';
particle.style.opacity = '0';
document.body.appendChild(particle);
particles.push({ el: particle, delay });
}
return particles;
}
function createTrail(startX, startY, endX, endY) {
const trailCount = 0;
const trails = [];
const dx = endX - startX;
const dy = endY - startY;
const dist = Math.hypot(dx, dy);
const angle = Math.atan2(dy, dx);
for (let i = 0; i < trailCount; i++) {
const trail = document.createElement('div');
trail.className = 'particle-trail';
const offset = (Math.random() - 0.5) * 80;
const perpX = -Math.sin(angle) * offset;
const perpY = Math.cos(angle) * offset;
trail.style.width = dist + 'px';
trail.style.background = `linear-gradient(90deg,
rgba(255,255,255,0) 0%,
rgba(200,230,255,${0.2 + Math.random() * 0.5}) 50%,
rgba(255,255,255,0) 100%)`;
trail.style.left = (startX + 90 + perpX) + 'px';
trail.style.top = (startY + 25 + perpY) + 'px';
trail.style.transform = `rotate(${angle}rad)`;
trail.style.opacity = '0';
document.body.appendChild(trail);
trails.push(trail);
}
return trails;
}
function animateScatter(particles, centerX, centerY) {
particles.forEach((p, i) => {
const angle = (Math.PI * 2 * i) / particles.length + Math.random() * 0.5;
const speed = 50 + Math.random() * 200;
const spreadX = Math.cos(angle) * speed;
const spreadY = Math.sin(angle) * speed;
setTimeout(() => {
p.el.style.transition = 'all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
p.el.style.opacity = '1';
p.el.style.transform = `translate(${spreadX}px, ${spreadY}px) scale(0)`;
}, p.delay * 1000);
});
setTimeout(() => {
particles.forEach(p => p.el.remove());
}, 1000);
}
function animateCondense(particles, centerX, centerY) {
const vw = window.innerWidth;
const vh = window.innerHeight;
particles.forEach((p, i) => {
const edge = Math.floor(Math.random() * 4);
let startX, startY;
const margin = 50;
switch(edge) {
case 0:
startX = Math.random() * vw;
startY = -margin - Math.random() * 200;
break;
case 1:
startX = vw + margin + Math.random() * 200;
startY = Math.random() * vh;
break;
case 2:
startX = Math.random() * vw;
startY = vh + margin + Math.random() * 200;
break;
case 3:
startX = -margin - Math.random() * 200;
startY = Math.random() * vh;
break;
}
p.el.style.left = startX + 'px';
p.el.style.top = startY + 'px';
p.el.style.opacity = '0';
p.el.style.transform = 'scale(2)';
setTimeout(() => {
p.el.style.transition = 'all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)';
p.el.style.left = (centerX + 90 + (Math.random() - 0.5) * 60) + 'px';
p.el.style.top = (centerY + 25 + (Math.random() - 0.5) * 40) + 'px';
p.el.style.opacity = '1';
p.el.style.transform = 'scale(0)';
}, p.delay * 500 + 50);
});
setTimeout(() => {
particles.forEach(p => p.el.remove());
}, 800);
}
function animateTrails(trails) {
trails.forEach((trail, i) => {
setTimeout(() => {
trail.style.transition = 'opacity 0.3s ease-out';
trail.style.opacity = '0.6';
}, i * 25);
setTimeout(() => {
trail.style.transition = 'opacity 0.5s ease-in';
trail.style.opacity = '0';
}, 150 + i * 25);
});
setTimeout(() => {
trails.forEach(t => t.remove());
}, 1000);
}
function showBubbleNow() {
const text = bubbleTexts[Math.floor(Math.random() * bubbleTexts.length)];
chatBubble.textContent = text;
updateBubblePosition();
chatBubble.classList.remove('hide');
chatBubble.classList.add('show');
if (bubbleTimer) clearTimeout(bubbleTimer);
bubbleTimer = setTimeout(() => {
chatBubble.classList.remove('show');
chatBubble.classList.add('hide');
}, 2500);
}
function teleport() {
if (isTeleporting) return;
isTeleporting = true;
clickCount++;
const shouldShowBubble = clickCount % 5 === 0;
btn.classList.remove('shake');
requestAnimationFrame(() => {
requestAnimationFrame(() => btn.classList.add('shake'));
});
shakeTimer = setTimeout(() => btn.classList.remove('shake'), 450);
const target = getRandomTarget();
const startX = currentX;
const startY = currentY;
const endX = target.x;
const endY = target.y;
const wrapperRect = btnWrapper.getBoundingClientRect();
const startScreenX = wrapperRect.left;
const startScreenY = wrapperRect.top;
chatBubble.classList.remove('show');
chatBubble.classList.add('hide');
btn.classList.add('scattering');
shadow.classList.add('hidden');
const scatterParticles = createParticles(startScreenX, startScreenY, 0, false);
animateScatter(scatterParticles, startScreenX, startScreenY);
setTimeout(() => {
currentX = endX;
currentY = endY;
btnWrapper.style.transform = `translate(${currentX}px, ${currentY}px)`;
requestAnimationFrame(() => {
const newRect = btnWrapper.getBoundingClientRect();
const endScreenX = newRect.left;
const endScreenY = newRect.top;
const condenseParticles = createParticles(endScreenX, endScreenY, 400, true);
animateCondense(condenseParticles, endScreenX, endScreenY);
setTimeout(() => {
btn.classList.remove('scattering');
btn.classList.add('condensing');
shadow.classList.remove('hidden');
setTimeout(() => {
btn.classList.remove('condensing');
isTeleporting = false;
if (shouldShowBubble) {
showBubbleNow();
}
}, 500);
}, 400);
});
}, 350);
}
btn.addEventListener('click', teleport);
btn.addEventListener('touchstart', (e) => { e.preventDefault(); teleport(); });
window.addEventListener('resize', () => {
if (shakeTimer) clearTimeout(shakeTimer);
if (bubbleTimer) clearTimeout(bubbleTimer);
currentX = 0; currentY = 0;
clickCount = 0;
isTeleporting = false;
btnWrapper.style.transform = 'translate(0, 0)';
btn.classList.remove('scattering', 'condensing', 'shake');
shadow.classList.remove('hidden');
chatBubble.classList.remove('show', 'hide', 'pos-left');
chatBubble.classList.add('pos-right');
document.querySelectorAll('.particle, .particle-trail, .spark').forEach(el => el.remove());
});
</script>
</body>
</html>
夜雨聆风