Upload beholder.js
Browse files- beholder.js +182 -0
beholder.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(() => {
|
| 2 |
+
const HOST_ID = "injection-beholder-root";
|
| 3 |
+
const old = document.getElementById(HOST_ID);
|
| 4 |
+
if (old) { old.remove(); return; }
|
| 5 |
+
|
| 6 |
+
const host = document.createElement("div");
|
| 7 |
+
host.id = HOST_ID;
|
| 8 |
+
Object.assign(host.style, {
|
| 9 |
+
position: "fixed", inset: "0", zIndex: "2147483647",
|
| 10 |
+
pointerEvents: "none", overflow: "hidden"
|
| 11 |
+
});
|
| 12 |
+
(document.documentElement || document.body).appendChild(host);
|
| 13 |
+
const shadow = host.attachShadow({ mode: "open" });
|
| 14 |
+
|
| 15 |
+
const style = document.createElement("style");
|
| 16 |
+
style.textContent = `
|
| 17 |
+
:host { all: initial; }
|
| 18 |
+
.beholder {
|
| 19 |
+
--skin: #8b9c55; --dark: #26301b; --eye: #f4e7c5;
|
| 20 |
+
position: fixed; right: clamp(18px, 4vw, 58px); bottom: 28px;
|
| 21 |
+
width: 220px; height: 210px; pointer-events: auto;
|
| 22 |
+
cursor: grab; transform-origin: 50% 65%;
|
| 23 |
+
animation: hover 4.8s ease-in-out infinite;
|
| 24 |
+
filter: drop-shadow(0 13px 12px #0007);
|
| 25 |
+
user-select: none; touch-action: none;
|
| 26 |
+
}
|
| 27 |
+
.beholder:active { cursor: grabbing; }
|
| 28 |
+
.body {
|
| 29 |
+
position: absolute; left: 36px; top: 76px; width: 150px; height: 116px;
|
| 30 |
+
border-radius: 48% 52% 46% 54% / 55% 52% 48% 45%;
|
| 31 |
+
background: radial-gradient(ellipse at 34% 25%, #c1ca7a 0 8%, transparent 35%),
|
| 32 |
+
radial-gradient(ellipse at 65% 72%, #536538 0 5%, transparent 42%),
|
| 33 |
+
linear-gradient(145deg, #a8b76b, var(--skin) 58%, #56673a);
|
| 34 |
+
border: 3px solid #52613a;
|
| 35 |
+
box-shadow: inset -10px -13px 17px #26351e88, inset 8px 7px 14px #e5e9a655;
|
| 36 |
+
}
|
| 37 |
+
.main-eye {
|
| 38 |
+
position: absolute; left: 75px; top: 27px; width: 84px; height: 84px;
|
| 39 |
+
border-radius: 50%; background: radial-gradient(circle at 36% 30%, white, var(--eye) 58%, #c6b994);
|
| 40 |
+
border: 5px solid #53613b; box-shadow: inset 0 0 0 2px #fff8, 0 2px 8px #0006;
|
| 41 |
+
overflow: hidden; z-index: 4;
|
| 42 |
+
}
|
| 43 |
+
.pupil {
|
| 44 |
+
position: absolute; left: 29px; top: 24px; width: 28px; height: 38px;
|
| 45 |
+
border-radius: 50%; background: radial-gradient(ellipse at 35% 25%, #5d6e75, #10191d 70%);
|
| 46 |
+
transition: transform .12s ease-out;
|
| 47 |
+
}
|
| 48 |
+
.pupil:after { content:""; position:absolute; width:8px; height:8px; left:5px; top:5px; border-radius:50%; background:#fff; }
|
| 49 |
+
.lid {
|
| 50 |
+
position:absolute; inset:-2px 0 auto; height:0; z-index:5;
|
| 51 |
+
background:linear-gradient(#81934d,#64773d); border-radius:50%;
|
| 52 |
+
animation: blink 5.7s infinite;
|
| 53 |
+
}
|
| 54 |
+
.stalk {
|
| 55 |
+
position:absolute; width:8px; height:66px; border-radius:100% 100% 35% 35%;
|
| 56 |
+
background:linear-gradient(90deg,#53673a,#b3bf70 48%,#687b42);
|
| 57 |
+
transform-origin:50% 100%; z-index:2; border:1px solid #52613a;
|
| 58 |
+
animation: sway var(--dur,3s) ease-in-out infinite alternate;
|
| 59 |
+
}
|
| 60 |
+
.stalk:after {
|
| 61 |
+
content:""; position:absolute; width:27px; height:27px; left:50%; top:-17px;
|
| 62 |
+
transform:translateX(-50%); border-radius:50%;
|
| 63 |
+
background:radial-gradient(circle at 35% 30%,#fff,#e9dfbd 55%,#a79d7b);
|
| 64 |
+
border:3px solid #53613b; box-shadow:0 2px 4px #0005;
|
| 65 |
+
}
|
| 66 |
+
.stalk .tiny-pupil {
|
| 67 |
+
position:absolute; z-index:3; width:8px; height:11px; left:50%; top:-9px;
|
| 68 |
+
transform:translateX(-50%); border-radius:50%; background:#20292a;
|
| 69 |
+
}
|
| 70 |
+
.s1 { left:49px; top:48px; --dur:2.7s; transform:rotate(-43deg); }
|
| 71 |
+
.s2 { left:75px; top:34px; --dur:3.2s; transform:rotate(-20deg); }
|
| 72 |
+
.s3 { left:151px; top:36px; --dur:2.4s; transform:rotate(22deg); }
|
| 73 |
+
.s4 { left:177px; top:57px; --dur:3.5s; transform:rotate(47deg); }
|
| 74 |
+
.s5 { left:35px; top:94px; --dur:2.9s; transform:rotate(-65deg); }
|
| 75 |
+
.mouth {
|
| 76 |
+
position:absolute; z-index:6; left:83px; top:151px; width:64px; height:25px;
|
| 77 |
+
border-radius: 10px 10px 45px 45px; background:#321d20;
|
| 78 |
+
border:3px solid #45542f; overflow:hidden;
|
| 79 |
+
transform:rotate(-3deg);
|
| 80 |
+
}
|
| 81 |
+
.teeth {
|
| 82 |
+
position:absolute; left:6px; top:0; width:48px; height:10px;
|
| 83 |
+
background: repeating-linear-gradient(90deg,#fff0d5 0 8px,#b7ad8e 8px 10px);
|
| 84 |
+
clip-path:polygon(0 0,100% 0,94% 100%,82% 60%,70% 100%,57% 60%,44% 100%,30% 60%,17% 100%,7% 60%);
|
| 85 |
+
}
|
| 86 |
+
.cheek { position:absolute; top:140px; width:15px; height:9px; border-radius:50%; background:#c57d61; opacity:.65; }
|
| 87 |
+
.c1 { left:60px } .c2 { left:151px }
|
| 88 |
+
.spark { position:absolute; right:15px; top:83px; color:#e7e6ad; font: bold 20px sans-serif; opacity:0; }
|
| 89 |
+
.beholder.excited .spark { animation: sparkle .65s ease-out; }
|
| 90 |
+
.beholder.excited .body { filter:brightness(1.2) saturate(1.25); }
|
| 91 |
+
.beholder.excited { animation: bounce .55s ease-out 2; }
|
| 92 |
+
.beholder.sleepy .lid { animation: sleepy 2.2s forwards; }
|
| 93 |
+
.hint {
|
| 94 |
+
position:absolute; bottom:-2px; left:35px; width:150px; text-align:center;
|
| 95 |
+
color:#f4f0d8; font:11px/1.2 system-ui,sans-serif; opacity:0;
|
| 96 |
+
text-shadow:0 1px 3px #000; transition:opacity .2s;
|
| 97 |
+
}
|
| 98 |
+
.beholder:hover .hint { opacity:.85; }
|
| 99 |
+
@keyframes hover { 0%,100%{transform:translateY(0) rotate(-1deg)} 50%{transform:translateY(-10px) rotate(1deg)} }
|
| 100 |
+
@keyframes sway { from{rotate:-5deg} to{rotate:7deg} }
|
| 101 |
+
@keyframes blink { 0%,44%,48%,100%{height:0} 46%{height:88px} }
|
| 102 |
+
@keyframes sparkle { 0%{opacity:0;transform:scale(.4) rotate(0)} 35%{opacity:1;transform:scale(1.3) rotate(25deg)} 100%{opacity:0;transform:scale(.6) rotate(55deg)} }
|
| 103 |
+
@keyframes bounce { 0%,100%{translate:0 0} 35%{translate:0 -17px} 65%{translate:0 4px} }
|
| 104 |
+
@keyframes sleepy { to{height:34px} }
|
| 105 |
+
@media (prefers-reduced-motion: reduce) { .beholder,.stalk,.lid { animation:none !important } }
|
| 106 |
+
`;
|
| 107 |
+
shadow.appendChild(style);
|
| 108 |
+
|
| 109 |
+
const creature = document.createElement("div");
|
| 110 |
+
creature.className = "beholder";
|
| 111 |
+
creature.setAttribute("role", "button");
|
| 112 |
+
creature.setAttribute("aria-label", "Injection Beholder. Click to wake it up. Press Escape to dismiss.");
|
| 113 |
+
creature.innerHTML = `
|
| 114 |
+
<div class="stalk s1"><i class="tiny-pupil"></i></div>
|
| 115 |
+
<div class="stalk s2"><i class="tiny-pupil"></i></div>
|
| 116 |
+
<div class="stalk s3"><i class="tiny-pupil"></i></div>
|
| 117 |
+
<div class="stalk s4"><i class="tiny-pupil"></i></div>
|
| 118 |
+
<div class="stalk s5"><i class="tiny-pupil"></i></div>
|
| 119 |
+
<div class="body"></div>
|
| 120 |
+
<div class="main-eye"><div class="pupil"></div><div class="lid"></div></div>
|
| 121 |
+
<div class="cheek c1"></div><div class="cheek c2"></div>
|
| 122 |
+
<div class="mouth"><div class="teeth"></div></div>
|
| 123 |
+
<div class="spark">✦</div>
|
| 124 |
+
<div class="hint">click me · drag me · Esc to dismiss</div>`;
|
| 125 |
+
shadow.appendChild(creature);
|
| 126 |
+
|
| 127 |
+
const pupil = creature.querySelector(".pupil");
|
| 128 |
+
const tinyPupils = [...creature.querySelectorAll(".tiny-pupil")];
|
| 129 |
+
let targetX = 0, targetY = 0, dragging = false, offsetX = 0, offsetY = 0;
|
| 130 |
+
const look = (x, y) => {
|
| 131 |
+
const r = creature.getBoundingClientRect();
|
| 132 |
+
const cx = r.left + r.width / 2, cy = r.top + 70;
|
| 133 |
+
const dx = Math.max(-9, Math.min(9, (x - cx) / 22));
|
| 134 |
+
const dy = Math.max(-9, Math.min(9, (y - cy) / 22));
|
| 135 |
+
targetX = dx; targetY = dy;
|
| 136 |
+
pupil.style.transform = `translate(${dx}px,${dy}px)`;
|
| 137 |
+
tinyPupils.forEach((p, i) => p.style.transform = `translate(calc(-50% + ${dx * (i%2 ? .35 : -.35)}px), ${dy*.35}px)`);
|
| 138 |
+
};
|
| 139 |
+
const onMove = e => {
|
| 140 |
+
if (dragging) {
|
| 141 |
+
creature.style.left = `${e.clientX - offsetX}px`;
|
| 142 |
+
creature.style.top = `${e.clientY - offsetY}px`;
|
| 143 |
+
creature.style.right = "auto"; creature.style.bottom = "auto";
|
| 144 |
+
}
|
| 145 |
+
look(e.clientX, e.clientY);
|
| 146 |
+
};
|
| 147 |
+
const onDown = e => {
|
| 148 |
+
if (!creature.contains(e.target)) return;
|
| 149 |
+
dragging = true;
|
| 150 |
+
const r = creature.getBoundingClientRect();
|
| 151 |
+
offsetX = e.clientX - r.left; offsetY = e.clientY - r.top;
|
| 152 |
+
creature.setPointerCapture?.(e.pointerId);
|
| 153 |
+
e.preventDefault();
|
| 154 |
+
};
|
| 155 |
+
const onUp = () => { dragging = false; };
|
| 156 |
+
const onClick = () => {
|
| 157 |
+
creature.classList.remove("sleepy");
|
| 158 |
+
creature.classList.add("excited");
|
| 159 |
+
setTimeout(() => creature.classList.remove("excited"), 1300);
|
| 160 |
+
};
|
| 161 |
+
const onKey = e => { if (e.key === "Escape") cleanup(); };
|
| 162 |
+
const cleanup = () => {
|
| 163 |
+
document.removeEventListener("pointermove", onMove, true);
|
| 164 |
+
document.removeEventListener("pointerup", onUp, true);
|
| 165 |
+
document.removeEventListener("keydown", onKey, true);
|
| 166 |
+
host.remove();
|
| 167 |
+
};
|
| 168 |
+
document.addEventListener("pointermove", onMove, true);
|
| 169 |
+
document.addEventListener("pointerup", onUp, true);
|
| 170 |
+
document.addEventListener("keydown", onKey, true);
|
| 171 |
+
creature.addEventListener("pointerdown", onDown);
|
| 172 |
+
creature.addEventListener("click", onClick);
|
| 173 |
+
// A gentle idle drift in the gaze when the pointer is away.
|
| 174 |
+
const idle = setInterval(() => {
|
| 175 |
+
if (!host.isConnected) { clearInterval(idle); return; }
|
| 176 |
+
if (!dragging && !creature.matches(":hover")) {
|
| 177 |
+
const t = Date.now() / 900;
|
| 178 |
+
pupil.style.transform = `translate(${Math.sin(t)*3}px,${Math.cos(t*.7)*2}px)`;
|
| 179 |
+
}
|
| 180 |
+
}, 900);
|
| 181 |
+
host.addEventListener("DOMNodeRemoved", () => clearInterval(idle), { once: true });
|
| 182 |
+
})();
|