<div class="voice-app">
<canvas id="ripple-canvas"></canvas>
<div class="mic-container">
<button class="mic-btn" id="mic-btn" aria-label="Microphone">
<svg viewBox="0 0 24 24" width="36" height="36" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
</button>
<div class="helper-text" id="help-text">Hold to speak</div>
</div>
</div>
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
margin: 0;
background: #0f172a;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.voice-app {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
#ripple-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
}
.mic-container {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
/* Shift slightly to ensure we test dynamic alignment */
margin-top: -50px;
}
.mic-btn {
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #ffffff;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s, box-shadow 0.2s;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}
.mic-btn.active {
transform: scale(0.9);
background: #38bdf8;
border-color: #7dd3fc;
color: #0f172a;
box-shadow: 0 0 50px rgba(56, 189, 248, 0.5);
}
.helper-text {
margin-top: 30px;
color: #94a3b8;
font-size: 1.125rem;
letter-spacing: 1px;
transition: color 0.2s, text-shadow 0.2s;
}
const canvas = document.getElementById("ripple-canvas");
const ctx = canvas.getContext("2d");
const micBtn = document.getElementById("mic-btn");
const helpText = document.getElementById("help-text");
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
window.addEventListener("resize", function() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
});
const ripples = new Array();
let isListening = false;
let rippleTimer = null;
function spawnRipple() {
/* Precisely calculate the button center directly from the DOM */
const rect = micBtn.getBoundingClientRect();
const centerX = rect.left + (rect.width / 2);
const centerY = rect.top + (rect.height / 2);
const r = new Object();
r.x = centerX;
r.y = centerY;
/* Start the ripple exactly at the button edge */
r.radius = rect.width / 2;
r.alpha = 1;
/* Add randomness to mimic sound modulation */
r.speed = Math.random() * 2 + 3;
ripples.push(r);
}
micBtn.addEventListener("pointerdown", function(e) {
isListening = true;
micBtn.classList.add("active");
micBtn.setPointerCapture(e.pointerId);
helpText.innerText = "Listening...";
helpText.style.color = "#38bdf8";
helpText.style.textShadow = "0 0 10px rgba(56,189,248,0.5)";
/* Spawn ripples rapidly while holding */
rippleTimer = setInterval(spawnRipple, 120);
});
function stopListening() {
isListening = false;
micBtn.classList.remove("active");
helpText.innerText = "Hold to speak";
helpText.style.color = "#94a3b8";
helpText.style.textShadow = "none";
clearInterval(rippleTimer);
}
micBtn.addEventListener("pointerup", stopListening);
micBtn.addEventListener("pointercancel", stopListening);
function animate() {
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < ripples.length; i++) {
const r = ripples.at(i);
r.radius += r.speed;
r.alpha -= 0.015;
if (r.alpha > 0) {
ctx.beginPath();
ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2);
ctx.strokeStyle = "rgba(56, 189, 248, " + r.alpha + ")";
ctx.lineWidth = 3;
ctx.stroke();
}
}
/* Clean up dead ripples safely */
while (ripples.length > 0 && ripples.at(0).alpha <= 0) {
ripples.shift();
}
window.requestAnimationFrame(animate);
}
animate();