Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 227 additions & 8 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,43 @@
<title>STFU</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>🤫</text></svg>">
<style>
* {
box-sizing: border-box;
}

body {
margin: 0;
height: 100vh;
background: #000;
font-family: monospace;
color: #fff;
}

button,
a {
font: inherit;
}

.experience {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
background: #000;
font-family: monospace;
color: #fff;
font-family: inherit;
cursor: pointer;
}

.experience:focus-visible {
outline: 1px solid #777;
outline-offset: -8px;
}

.emoji {
font-size: 80px;
margin-bottom: 20px;
Expand All @@ -35,15 +59,167 @@
font-size: 16px;
color: #666;
}

.about-button {
position: fixed;
left: 50%;
bottom: max(24px, env(safe-area-inset-bottom));
z-index: 2;
transform: translateX(-50%);
padding: 10px 14px;
border: 1px solid #333;
background: #000;
color: #888;
cursor: pointer;
}

.about-button:hover,
.about-button:focus-visible {
border-color: #777;
color: #fff;
outline: none;
}

.about-overlay {
position: fixed;
inset: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(0, 0, 0, 0.88);
}

.about-overlay[hidden] {
display: none;
}

.about-panel {
position: relative;
width: min(100%, 620px);
max-height: calc(100svh - 40px);
overflow-y: auto;
padding: 36px;
border: 1px solid #292929;
background: #080808;
}

.about-panel h2 {
margin: 0 48px 28px 0;
font-size: 24px;
}

.about-panel p {
margin: 0 0 18px;
color: #aaa;
font-size: 15px;
line-height: 1.7;
}

.about-panel strong {
color: #fff;
}

.about-links {
display: flex;
flex-wrap: wrap;
gap: 14px 22px;
padding-top: 10px;
border-top: 1px solid #222;
}

.about-links a {
color: #fff;
text-underline-offset: 4px;
}

.close-button {
position: absolute;
top: 20px;
right: 20px;
border: 0;
background: transparent;
color: #777;
font-size: 20px;
cursor: pointer;
}

.close-button:hover,
.close-button:focus-visible {
color: #fff;
outline: 1px solid #777;
outline-offset: 4px;
}

@media (max-width: 480px) {
.about-panel {
padding: 28px 22px;
}

.about-panel h2 {
font-size: 21px;
}
}
</style>
</head>

<body>
<div class="emoji">🤫</div>
<div class="text">STFU</div>
<div class="hint">tap to shut them up</div>
<button class="experience" id="experience" type="button" aria-label="start delayed audio">
<span class="emoji">🤫</span>
<span class="text">STFU</span>
<span class="hint">tap to shut them up</span>
</button>

<button class="about-button" id="about-button" type="button" aria-haspopup="dialog" aria-controls="about" aria-expanded="false">
why this exists
</button>

<div class="about-overlay" id="about" role="dialog" aria-modal="true" aria-labelledby="about-title" hidden>
<article class="about-panel">
<button class="close-button" id="close-about" type="button" aria-label="close story">×</button>
<h2 id="about-title">the story behind STFU</h2>

<p>
i was at mumbai airport. some dude was watching reels on full volume and laughing loudly.
asking nicely doesn't work anymore. me being me, i didn't have the courage to speak up.
</p>

<p>
so i asked claude. it spat out a working version in one prompt. annoyingly, it worked.
<strong>STFU plays back whatever it hears with a two-second delay.</strong>
hearing your own voice arrive late makes talking surprisingly difficult. something something
auditory feedback loop. i'm not a neuroscientist. i just know it works.
</p>

<p>
straight-up honest: i originally called it “make-it-stop.” then i saw
<a href="https://tim.darcet.fr/shutthefuckup/" target="_blank" rel="noopener noreferrer">Tim Darcet</a>
had built something similar and called it STFU. much better name, so i stole it. sorry not sorry.
</p>

<p>made with spite and the Web Audio API.</p>

<nav class="about-links" aria-label="project links">
<a href="https://x.com/the2ndfloorguy/status/2011734249871954188" target="_blank" rel="noopener noreferrer">the original post ↗</a>
<a href="https://x.com/the2ndfloorguy" target="_blank" rel="noopener noreferrer">@the2ndfloorguy ↗</a>
<a href="https://pankajtanwar.in" target="_blank" rel="noopener noreferrer">pankajtanwar.in ↗</a>
</nav>
</article>
</div>

<script>
document.body.addEventListener('click', async () => {
const experience = document.querySelector('#experience');
const hint = document.querySelector('.hint');
const aboutButton = document.querySelector('#about-button');
const about = document.querySelector('#about');
const closeAboutButton = document.querySelector('#close-about');

let hasStarted = false;

async function startExperience() {
if (hasStarted) return;
hasStarted = true;
const audioContext = new AudioContext();
const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioContext.createMediaStreamSource(mediaStream);
Expand All @@ -54,8 +230,51 @@
source.connect(delayNode);
delayNode.connect(gainNode);
gainNode.connect(audioContext.destination);
document.querySelector('.hint').style.display = 'none';
}, { once: true });
hint.style.display = 'none';
}

experience.addEventListener('click', startExperience);

function openAbout() {
about.hidden = false;
experience.inert = true;
aboutButton.inert = true;
aboutButton.setAttribute('aria-expanded', 'true');
closeAboutButton.focus();
}

function closeAbout() {
about.hidden = true;
experience.inert = false;
aboutButton.inert = false;
aboutButton.setAttribute('aria-expanded', 'false');
aboutButton.focus();
}

aboutButton.addEventListener('click', openAbout);
closeAboutButton.addEventListener('click', closeAbout);
about.addEventListener('click', (event) => {
if (event.target === about) closeAbout();
});
document.addEventListener('keydown', (event) => {
if (about.hidden) return;
if (event.key === 'Escape') {
closeAbout();
return;
}
if (event.key !== 'Tab') return;

const focusable = [...about.querySelectorAll('button, a[href]')];
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
</script>
</body>

Expand Down