两个自制交互按钮
2026-06-02
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Starmaker Heads</title>
<style>
:root {
--border: 1px solid black;
}
body {
width: 100vw;
height: 100vh;
background: #f4e9ea;
display: flex;
justify-content: center;
align-items: center;
}
.container {
display: flex;
gap: 40px;
align-items: flex-end;
}
.head {
border-radius: 10px;
border: var(--border);
height: 100px;
position: relative;
}
.ear {
height: 50px;
width: 40px;
border-radius: 50%;
border: var(--border);
position: absolute;
bottom: 15px;
left: -12px;
z-index: -1;
background: #3c4758;
overflow: hidden;
&:after {
content: "";
height: 35px;
width: 35px;
border-radius: 50%;
border: var(--border);
background: #4390a6;
position: absolute;
}
&.right {
transform: scaleX(-1);
left: unset;
right: -12px;
}
}
.first {
position: relative;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
touch-action: manipulation;
.head {
width: 120px;
background: linear-gradient(#d3cecf 30%, #8b8084);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.mouth {
height: 30px;
width: 100%;
background: linear-gradient(#d3cecf 40%, #a79ca0);
border-top: var(--border);
}
.face {
display: flex;
justify-content: space-between;
margin-left: -5px;
margin-right: -5px;
position: relative;
}
.eye {
height: 45px;
width: 45px;
border-radius: 50%;
border: var(--border);
background: linear-gradient(#b0a7aa, white);
position: relative;
overflow: hidden;
&:before {
content: "";
height: 33px;
width: 33px;
border-radius: 50%;
position: absolute;
top: 6px;
left: 6px;
background: #503641;
}
&:after {
content: "";
height: 22px;
width: 45px;
border-radius: 22px 22px 0 0;
background: #ebac6d;
position: absolute;
top: -2px;
left: 0;
border: var(--border);
}
}
.nose {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -2px;
height: 50px;
width: 50px;
border-radius: 50%;
border: var(--border);
background: #395267;
overflow: hidden;
&:after {
content: "";
width: 55px;
height: 100%;
border: var(--border);
border-radius: 50%;
position: absolute;
background: linear-gradient(#6fb0aa, #2f81a1);
top: -30%;
left: 50%;
transform: translateX(-50%);
}
}
.head-gear {
height: 40px;
width: 10px;
border: var(--border);
border-bottom: none;
margin: 0 auto;
position: relative;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background: linear-gradient(#d5d0d2 20%, #6a5e60 40%, #d5d0d2);
.button {
position: absolute;
height: 5px;
width: 15px;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: #6a5e60;
border: var(--border);
border-radius: 5px;
}
.paddle {
position: absolute;
filter: url("#goo");
right: 54px;
top: 10px;
&:before,
&:after {
content: "";
width: 50px;
height: 20px;
clip-path: polygon(15% 0, 100% 0, 0 100%);
position: absolute;
}
&:before {
background: #7e4640;
top: 5px;
}
&:after {
background: #ea9564;
}
&.reverse {
right: -44px;
top: 15px;
transform: scale(-1, -1);
}
}
}
}
.second {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
touch-action: manipulation;
.head {
width: 130px;
background: linear-gradient(#eb87b4 40%, #e44c8d);
display: flex;
justify-content: center;
align-items: center;
&:before,
&:after {
content: "";
height: 30px;
width: 45px;
border-radius: 15px;
border: var(--border);
position: absolute;
top: -15px;
z-index: -1;
}
&:before {
transform: rotate(-15deg);
left: 0;
background: linear-gradient(195deg, #eb87b4, #e44c8d, #903d5e 70%);
}
&:after {
transform: rotate(15deg);
right: 0;
background: linear-gradient(155deg, #eb87b4, #e44c8d, #903d5e 70%);
}
}
.face {
height: 75px;
width: 100px;
border: var(--border);
border-radius: 10px;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
background: linear-gradient(#3e2c32, #593744, #794b5d, #8a5f6d);
}
.forehead {
position: absolute;
height: 7px;
width: 100%;
border-bottom: var(--border);
background: #73394f;
z-index: 2;
}
.eyes {
display: flex;
justify-content: space-between;
margin-bottom: -20px;
margin-top: -22px;
gap: 25px;
.eye {
height: 40px;
width: 40px;
border-radius: 50%;
background: linear-gradient(#8b7d7d, white);
border: var(--border);
position: relative;
overflow: hidden;
&:before,
&:after {
content: "";
height: 28px;
width: 25px;
border-radius: 50%;
position: absolute;
top: 0;
background: linear-gradient(#433243, #594c70);
}
&:before {
left: 10px;
}
&:after {
right: 10px;
}
/* Lid sits hidden (height 0) above the static eye; the blink
animation grows it down to cover the eye, then back. */
.eyelid {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 0;
background: linear-gradient(#6e4154, #8a5a6d);
border-bottom: var(--border);
z-index: 2;
}
}
}
.brows {
display: flex;
justify-content: space-between;
gap: 20px;
height: 40px;
margin-top: -10px;
z-index: 1;
&:before,
&:after {
content: "";
height: 40px;
width: 50px;
border-radius: 15px;
border: var(--border);
background: linear-gradient(#4b313e, #6e4154, #8a5a6d, #956e7b);
}
&:before {
transform: rotate(-5deg);
}
&:after {
transform: rotate(5deg);
}
}
.mouth {
height: 40px;
width: 60px;
border: var(--border);
border-bottom: none;
border-top-left-radius: 30px;
border-top-right-radius: 30px;
background: linear-gradient(#c0afb4, #977080);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
.lips {
height: 7px;
width: 40px;
border-radius: 50%;
border-bottom: var(--border);
position: relative;
margin-top: 10px;
&:before,
&:after {
content: "";
height: 5px;
width: 0;
border-left: var(--border);
position: absolute;
transform: rotate(20deg);
top: 2px;
}
&:after {
transform: rotate(-20deg);
right: 0;
}
}
}
.nose {
position: absolute;
height: 20px;
width: 30px;
border-radius: 50%;
border: var(--border);
background: linear-gradient(#665fb4, #635398);
top: 55%;
transform: translateY(-50%);
overflow: hidden;
z-index: 1;
&:before {
content: "";
height: 20px;
width: 30px;
border: var(--border);
border-radius: inherit;
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(#4f97d3, #597fbb);
}
}
}
.third,
.fourth {
position: relative;
.head {
width: 100px;
position: relative;
&:before {
content: "";
height: 70px;
width: 70px;
border-radius: 10px;
border: var(--border);
position: absolute;
left: 15px;
bottom: 15px;
}
.face {
position: absolute;
top: 20px;
left: 15px;
height: 65px;
width: 70px;
background: linear-gradient(#d0684b, #efbd77);
border-radius: 10px;
border: var(--border);
overflow: hidden;
}
.eyes {
display: flex;
margin: 5px -10px 0;
justify-content: space-between;
.eye {
height: 35px;
width: 35px;
border: var(--border);
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-right-radius: 50%;
border-bottom-left-radius: 50%;
background: linear-gradient(#b0a7aa 60%, white);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
&:before {
content: "";
height: 22px;
width: 25px;
border-radius: 50%;
background: black;
}
&:after {
content: "";
position: absolute;
height: 18px;
width: 35px;
top: 0;
background: linear-gradient(#eeb974, #e58553);
border-bottom: var(--border);
}
}
}
.nose {
height: 35px;
width: 35px;
border-radius: 50%;
border: var(--border);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: linear-gradient(#af4970, #5c3041);
overflow: hidden;
z-index: 10;
&:after {
content: "";
height: 100%;
width: 100%;
border-radius: 50%;
border: var(--border);
position: absolute;
top: -8px;
background: linear-gradient(#ed7aa8, #c6497d);
}
}
}
.ear {
left: -15px;
background: #7f3854;
&:after {
background: linear-gradient(#e884af, #c3447a);
}
&.right {
left: unset;
right: -15px;
}
}
}
.third {
.head {
background: linear-gradient(#7bb8ad, #337493);
&:before {
background: #356178;
}
.mouth {
margin-left: 15px;
margin-top: 5px;
transform: rotate(180deg);
height: 20px;
width: 40px;
background:
linear-gradient(135deg, #eebb77 35%, transparent 1%) -10px 0,
linear-gradient(225deg, #eebb77 35%, transparent 1%) -10px 0,
linear-gradient(315deg, #e6a56b 35%, transparent 1%),
linear-gradient(45deg, #e6a56b 35%, transparent 1%);
background-size: 20px 20px;
background-color: black;
}
}
}
.fourth {
.head {
background: linear-gradient(#5498ce, #62569c);
&:before {
background: #5e4985;
}
.mouth {
height: 10px;
width: 50px;
border: var(--border);
border-radius: 10px;
margin-left: 10px;
background: linear-gradient(#513840, #7b4958);
&:after {
content: "";
width: 30px;
margin-left: 10px;
margin-top: 15px;
border-top: var(--border);
display: flex;
}
}
}
}
/* First head = press-and-hold button.
These animations run only while the head is pressed. On release, JS
lets each one finish its current cycle (see <script> at end of body),
so the propeller stops at a full turn and the eyes stop open — never
frozen mid-action. */
.first.spinning .paddles {
animation: rotate-fan 1s linear infinite;
}
.first.blinking .eye:after {
animation: close-eyes 0.9s ease-in-out infinite;
}
/* Second head = click button. One click blinks the eyes and pops the
eyebrows once; JS then clears the class so the next click replays it.
Static otherwise. */
.second.reacting .eyes .eye .eyelid {
animation: blink-second 0.6s ease-in-out;
}
.second.reacting .brows:before,
.second.reacting .brows:after {
animation: raise-eyebrows 0.6s ease-in-out;
}
@keyframes rotate-fan {
to {
transform: rotateY(360deg);
}
}
/* Blink lands at the start of each cycle, so a press blinks immediately
(the old version waited until 85% of a 3s cycle). The cycle ends back
at the open height (22px), which is also where it stops on release. */
@keyframes close-eyes {
0% {
height: 22px;
}
15% {
height: 50px;
}
30% {
height: 22px;
}
100% {
height: 22px;
}
}
/* Two quick pops starting immediately (was a slow 3s loop with a long
dead lead-in) — now a snappy one-shot for the click reaction. */
@keyframes raise-eyebrows {
0% {
height: 40px;
}
25% {
height: 30px;
}
50% {
height: 40px;
}
75% {
height: 30px;
}
100% {
height: 40px;
}
}
/* An eyelid drops over the (static) eye and lifts again = one blink.
The eye and pupils never move — only the lid. */
@keyframes blink-second {
0% {
height: 0;
}
35% {
height: 110%;
}
70% {
height: 0;
}
100% {
height: 0;
}
}
</style>
</head>
<body>
<div class="container">
<div class="first">
<div class="head-gear">
<div class="paddles">
<div class="paddle"></div>
<div class="paddle reverse"></div>
</div>
<div class="button"></div>
</div>
<div class="head">
<div class="face">
<div class="eye"></div>
<div class="eye"></div>
<div class="nose"></div>
</div>
<div class="mouth"></div>
</div>
<div class="ear"></div>
<div class="ear right"></div>
</div>
<div class="second">
<div class="head">
<div class="face">
<div class="forehead"></div>
<div class="brows"></div>
<div class="eyes">
<div class="eye"><div class="eyelid"></div></div>
<div class="eye"><div class="eyelid"></div></div>
</div>
<div class="mouth">
<div class="lips"></div>
</div>
<div class="nose"></div>
</div>
</div>
</div>
</div>
<svg
style="visibility: hidden; position: absolute"
width="0"
height="0"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
>
<defs>
<filter id="goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" />
<feColorMatrix
in="blur"
mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9"
result="goo"
/>
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
</defs>
</svg>
<script>
/* Make the first head a press-and-hold button.
Press -> propeller spins + eyes blink immediately (start at 0%).
Release -> each animation finishes its CURRENT cycle, then stops. */
(function () {
const first = document.querySelector(".first");
if (!first) return;
let pressing = false;
let stopSpin = false;
let stopBlink = false;
function press() {
if (pressing) return;
pressing = true;
stopSpin = false;
stopBlink = false;
first.classList.add("spinning", "blinking");
first.setAttribute("aria-pressed", "true");
}
function release() {
if (!pressing) return;
pressing = false;
/* Don't stop now — let each running animation reach its next cycle
boundary first (handled in the animationiteration listener below). */
if (first.classList.contains("spinning")) stopSpin = true;
if (first.classList.contains("blinking")) stopBlink = true;
first.setAttribute("aria-pressed", "false");
}
/* Fires at the boundary between cycles. If a stop was requested and the
user has let go, remove the animation here so it ends at rest:
propeller at a full turn, eyelids open — never cut off mid-action. */
first.addEventListener("animationiteration", function (e) {
if (pressing) return;
if (e.animationName === "rotate-fan" && stopSpin) {
first.classList.remove("spinning");
stopSpin = false;
} else if (e.animationName === "close-eyes" && stopBlink) {
first.classList.remove("blinking");
stopBlink = false;
}
});
/* Pointer (mouse + touch). Release is detected anywhere on the page, so
letting go after dragging off the head still counts. */
first.addEventListener("pointerdown", press);
window.addEventListener("pointerup", release);
window.addEventListener("pointercancel", release);
/* Keyboard: hold Space/Enter, like a real button. */
first.setAttribute("role", "button");
first.setAttribute("tabindex", "0");
first.setAttribute("aria-pressed", "false");
first.addEventListener("keydown", function (e) {
if (e.key === " " || e.key === "Enter") {
e.preventDefault(); /* stop Space from scrolling the page */
press();
}
});
first.addEventListener("keyup", function (e) {
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
release();
}
});
})();
/* Second head: click (mouse/touch) or Space/Enter to blink the eyes and
pop the eyebrows once. The class is reset on each trigger so rapid
clicks replay the reaction. */
(function () {
const second = document.querySelector(".second");
if (!second) return;
function react() {
second.classList.remove("reacting"); /* reset... */
void second.offsetWidth; /* ...force reflow to restart... */
second.classList.add("reacting"); /* ...then replay. */
}
/* Clear the class once the reaction finishes so it can run again. */
second.addEventListener("animationend", function () {
second.classList.remove("reacting");
});
second.addEventListener("click", react);
second.setAttribute("role", "button");
second.setAttribute("tabindex", "0");
second.addEventListener("keydown", function (e) {
if (e.repeat) return;
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
react();
}
});
})();
</script>
</body>
</html>

