Files
naomi e2d2f02d60
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 53s
feat: hairy button
2026-05-07 13:32:36 -07:00

428 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🦱 The Hairy Button 🦱</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Bungee+Shade&family=Fredoka:wght@400;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Fredoka', cursive;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #feca57, #ff9ff3);
background-size: 300% 300%;
animation: gradientShift 10s ease infinite;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Floating background elements */
.floating-emoji {
position: absolute;
font-size: 2rem;
animation: float 15s infinite ease-in-out;
opacity: 0.7;
user-select: none;
cursor: pointer;
transition: transform 0.3s;
z-index: 1;
}
.floating-emoji:hover {
transform: scale(1.5) rotate(360deg);
}
@keyframes float {
0%, 100% {
transform: translateY(0) translateX(0) rotate(0deg);
}
25% {
transform: translateY(-100px) translateX(50px) rotate(90deg);
}
50% {
transform: translateY(-50px) translateX(-50px) rotate(180deg);
}
75% {
transform: translateY(-150px) translateX(100px) rotate(270deg);
}
}
/* Hair canvas sits above the background but below the content */
#hair-canvas {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 5;
}
/* Main content container */
.container {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
padding: 2rem;
text-align: center;
}
/* Header */
header {
animation: bounceIn 1s ease-out;
}
@keyframes bounceIn {
0% {
transform: scale(0);
opacity: 0;
}
60% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
h1 {
font-family: 'Bungee Shade', cursive;
font-size: clamp(2.5rem, 8vw, 4.5rem);
color: #fff;
text-shadow: 3px 3px 0 #ff6b6b, 6px 6px 0 #4ecdc4, 9px 9px 0 #45b7d1;
margin-bottom: 0.75rem;
animation: wiggle 2s ease-in-out infinite;
}
@keyframes wiggle {
0%, 100% { transform: rotate(-3deg); }
50% { transform: rotate(3deg); }
}
.subtitle {
font-size: 1.3rem;
color: #fff;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
/* Button wrapper gives the canvas a reference element to float around */
.button-wrapper {
display: flex;
align-items: center;
justify-content: center;
/* Extra padding so hairs have breathing room before they reach the edge */
padding: 6rem;
}
#hairy-button {
font-family: 'Fredoka', cursive;
font-size: 1.4rem;
font-weight: 700;
padding: 18px 48px;
cursor: pointer;
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
color: #fff;
border: none;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
white-space: nowrap;
}
#hairy-button:hover {
background: linear-gradient(135deg, #6a3d96 0%, #5568d6 100%);
box-shadow: 0 12px 32px rgba(0,0,0,0.35);
}
#hairy-button:active {
transform: scale(0.96);
}
/* Click counter */
.counter {
font-size: 1.15rem;
color: #fff;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
min-height: 1.8rem;
transition: transform 0.15s ease;
}
.counter.bump {
transform: scale(1.15);
}
/* Back link */
.back-link {
font-size: 1rem;
color: #fff;
text-decoration: none;
font-weight: 700;
border-bottom: 2px solid transparent;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
transition: border-color 0.3s;
}
.back-link:hover {
border-color: #fff;
}
/* Cursor trail */
.cursor-trail {
position: fixed;
width: 20px;
height: 20px;
border-radius: 50%;
pointer-events: none;
z-index: 9999;
}
/* Accessibility */
#hairy-button:focus-visible {
outline: 3px solid #4ecdc4;
outline-offset: 4px;
}
.back-link:focus-visible {
outline: 3px solid #4ecdc4;
outline-offset: 3px;
border-radius: 2px;
}
</style>
</head>
<body>
<!-- Hair-themed floating background elements -->
<div class="floating-emoji" style="top: 8%; left: 8%; animation-delay: 0s;">🦱</div>
<div class="floating-emoji" style="top: 15%; left: 82%; animation-delay: 2s;">✂️</div>
<div class="floating-emoji" style="top: 65%; left: 6%; animation-delay: 4s;">💈</div>
<div class="floating-emoji" style="top: 72%; left: 88%; animation-delay: 6s;">🪮</div>
<div class="floating-emoji" style="top: 35%; left: 92%; animation-delay: 8s;">🦱</div>
<div class="floating-emoji" style="top: 82%; left: 45%; animation-delay: 10s;">✂️</div>
<div class="floating-emoji" style="top: 50%; left: 3%; animation-delay: 12s;">💈</div>
<div class="floating-emoji" style="top: 20%; left: 55%; animation-delay: 14s;">🪮</div>
<canvas id="hair-canvas"></canvas>
<div class="container">
<header>
<h1>The Hairy Button</h1>
<p class="subtitle">Click it. Go on. See what happens~</p>
</header>
<div class="button-wrapper">
<button id="hairy-button">Click me!</button>
</div>
<p class="counter" id="counter"></p>
<a href="/" class="back-link">← Back to the Carnival</a>
</div>
<script>
const canvas = document.getElementById('hair-canvas');
const ctx = canvas.getContext('2d');
const button = document.getElementById('hairy-button');
const counter = document.getElementById('counter');
const HAIR_COUNT = 350;
const GROWTH_PER_CLICK = 7;
let hairLength = 0;
let clickCount = 0;
let hairs = [];
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function generateHairs() {
hairs = [];
const rect = button.getBoundingClientRect();
const w = rect.width;
const h = rect.height;
const perimeter = 2 * (w + h);
for (let i = 0; i < HAIR_COUNT; i++) {
const dist = (i / HAIR_COUNT) * perimeter;
let x, y, baseAngle;
if (dist < w) {
x = rect.left + dist;
y = rect.top;
baseAngle = -Math.PI / 2;
} else if (dist < w + h) {
x = rect.right;
y = rect.top + (dist - w);
baseAngle = 0;
} else if (dist < 2 * w + h) {
x = rect.right - (dist - w - h);
y = rect.bottom;
baseAngle = Math.PI / 2;
} else {
x = rect.left;
y = rect.bottom - (dist - 2 * w - h);
baseAngle = Math.PI;
}
const r = 50 + Math.floor(Math.random() * 30);
const g = 30 + Math.floor(Math.random() * 20);
const b = 10 + Math.floor(Math.random() * 15);
const a = (0.55 + Math.random() * 0.4).toFixed(2);
hairs.push({
x,
y,
angle: baseAngle + (Math.random() - 0.5) * 0.8,
lengthFactor: 0.5 + Math.random() * 1.0,
curvature: (Math.random() - 0.5) * 1.4,
thickness: 0.3 + Math.random() * 0.55,
colour: `rgba(${r}, ${g}, ${b}, ${a})`,
});
}
}
function drawHairs() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (hairLength <= 0) return;
hairs.forEach(hair => {
const length = hairLength * hair.lengthFactor;
const endX = hair.x + Math.cos(hair.angle) * length;
const endY = hair.y + Math.sin(hair.angle) * length;
const midX = hair.x + Math.cos(hair.angle) * length * 0.5;
const midY = hair.y + Math.sin(hair.angle) * length * 0.5;
const perpX = Math.cos(hair.angle + Math.PI / 2);
const perpY = Math.sin(hair.angle + Math.PI / 2);
const ctrlX = midX + perpX * hair.curvature * length * 0.3;
const ctrlY = midY + perpY * hair.curvature * length * 0.3;
ctx.beginPath();
ctx.moveTo(hair.x, hair.y);
ctx.quadraticCurveTo(ctrlX, ctrlY, endX, endY);
ctx.strokeStyle = hair.colour;
ctx.lineWidth = hair.thickness;
ctx.lineCap = 'round';
ctx.stroke();
});
}
function hairinessLabel(length) {
if (length === 0) return '';
if (length < 20) return 'It\'s developing some stubble... 🪒';
if (length < 50) return 'Getting a little scruffy! 🧔';
if (length < 100) return 'Positively fluffy! 🦱';
if (length < 180) return 'It needs a trim. 💈';
if (length < 280) return 'The button is a whole vibe now. ✂️';
if (length < 400) return 'This button has seen things. 😳';
return 'Someone PLEASE call a barber!! 🆘';
}
function updateCounter() {
const label = hairinessLabel(hairLength);
counter.textContent = label
? `Clicks: ${clickCount}${label}`
: '';
}
function bumpCounter() {
counter.classList.remove('bump');
// Force reflow so re-adding the class triggers the transition again
void counter.offsetWidth;
counter.classList.add('bump');
}
button.addEventListener('click', () => {
clickCount++;
hairLength += GROWTH_PER_CLICK;
drawHairs();
updateCounter();
bumpCounter();
});
window.addEventListener('resize', () => {
resizeCanvas();
generateHairs();
drawHairs();
});
// Cursor trail
const trails = [];
const maxTrails = 10;
document.addEventListener('mousemove', (e) => {
if (trails.length >= maxTrails) {
trails.shift().remove();
}
const trail = document.createElement('div');
trail.className = 'cursor-trail';
trail.style.left = e.clientX + 'px';
trail.style.top = e.clientY + 'px';
trail.style.background = `hsl(${Math.random() * 360}, 100%, 70%)`;
document.body.appendChild(trail);
trails.push(trail);
setTimeout(() => {
trail.style.transform = 'scale(0)';
trail.style.transition = 'transform 0.3s ease';
setTimeout(() => {
const index = trails.indexOf(trail);
if (index > -1) trails.splice(index, 1);
trail.remove();
}, 300);
}, 100);
});
// Click floating emojis to change them
const hairEmojis = ['🦱', '✂️', '💈', '🪮', '🧔', '🪒', '🦲', '💇'];
document.querySelectorAll('.floating-emoji').forEach(emoji => {
emoji.addEventListener('click', function() {
this.textContent = hairEmojis[Math.floor(Math.random() * hairEmojis.length)];
});
});
// Initialise
resizeCanvas();
generateHairs();
drawHairs();
// Console easter egg
console.log('%c🦱 The Hairy Button 🦱', 'font-size: 24px; color: #764ba2; font-weight: bold;');
console.log('%cYou opened the console instead of clicking the button?? weirdo', 'font-size: 14px; color: #4ecdc4;');
</script>
</body>
<script defer="" src="https://analytics.nhcarrigan.com/js/pa-YUXAn1vhhRttySUAw_LMN.js"></script>
<script>
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
plausible.init({
customProperties: {
domain: "silly.nhcarrigan.com",
page: "The Hairy Button",
path: "/hairy-button/",
},
})
</script>
<script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3569924701890974" crossorigin="anonymous"></script>
</html>