← back to Crazy News Channel Shadowman
assets/eyes.js
62 lines
/* eyes.js — zero-dependency "googly eyes follow the mouse" drop-in.
* Usage: <div data-eyes></div><script src="eyes.js"></script>
* Options (data-attributes on the host):
* data-eye-size="200" eye diameter px (default 200)
* data-pupil-ratio="0.38" pupil diameter / eye diameter
* data-gap="8" px between eyes
* data-eye-color="#FAF0DF" data-pupil-color="#000"
* data-corner="bottom-right" pin the eyes fixed in a viewport corner
* (top-left|top-right|bottom-left|bottom-right)
* data-offset-x="16px" data-offset-y="16px" inset from that corner (any CSS length/calc)
* data-z="25" z-index when pinned
* The pupil centre is clamped to (R_eye - R_pupil) so it never leaves the eye. */
(function () {
var CSS = '.eyes-rig{display:flex;justify-content:center;align-items:center}' +
'.eyes-rig .eye{position:relative;border-radius:50%;overflow:hidden;flex:none}' +
'.eyes-rig .pupil{position:absolute;left:50%;top:50%;border-radius:50%;' +
'transition:transform .08s linear;will-change:transform}' +
'.eyes-rig.eyes-pinned{position:fixed;pointer-events:none;filter:drop-shadow(0 2px 4px rgba(0,0,0,.35))}' +
'@media (prefers-reduced-motion:reduce){.eyes-rig .pupil{transition:none}}';
if (!document.getElementById('eyes-rig-css')) {
var s = document.createElement('style'); s.id = 'eyes-rig-css'; s.textContent = CSS;
document.head.appendChild(s);
}
var pupils = [];
function build(host) {
if (host.__eyes) return; host.__eyes = true;
var d = host.dataset, size = +d.eyeSize || 200, ratio = +d.pupilRatio || 0.38;
var p = size * ratio;
host.classList.add('eyes-rig'); host.style.gap = (d.gap || 8) + 'px';
host.setAttribute('aria-hidden', 'true');
if (d.corner) {
var c = d.corner.split('-'), st = host.style;
host.classList.add('eyes-pinned'); st.zIndex = d.z || 25;
st[c[0] === 'top' ? 'top' : 'bottom'] = d.offsetY || '16px';
st[c[1] === 'left' ? 'left' : 'right'] = d.offsetX || '16px';
}
for (var i = 0; i < 2; i++) {
var eye = document.createElement('div'); eye.className = 'eye';
eye.style.cssText = 'width:' + size + 'px;height:' + size + 'px;background:' + (d.eyeColor || 'var(--eye-white,#FAF0DF)');
var pu = document.createElement('div'); pu.className = 'pupil';
pu.style.cssText = 'width:' + p + 'px;height:' + p + 'px;margin:' + (-p / 2) + 'px 0 0 ' + (-p / 2) + 'px;background:' + (d.pupilColor || 'var(--pupil,#000)');
eye.appendChild(pu); host.appendChild(eye);
pupils.push({ eye: eye, pupil: pu, max: (size - p) / 2 - 2 });
}
}
function look(x, y) {
pupils.forEach(function (o) {
var r = o.eye.getBoundingClientRect();
var dx = x - (r.left + r.width / 2), dy = y - (r.top + r.height / 2);
var dist = Math.hypot(dx, dy), k = dist > o.max ? o.max / dist : 1;
o.pupil.style.transform = 'translate(' + dx * k + 'px,' + dy * k + 'px)';
});
}
function init() {
document.querySelectorAll('[data-eyes]').forEach(build);
addEventListener('pointermove', function (e) { look(e.clientX, e.clientY); }, { passive: true });
addEventListener('touchmove', function (e) { var t = e.touches[0]; if (t) look(t.clientX, t.clientY); }, { passive: true });
}
window.Eyes = { mount: function (el) { build(el); } };
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();
})();