javascript – Creating a CSS3 Blinking Eyelid Effect

Question:

I am trying to create a countdown number idle screen that shows the eye along with the eyelid, and the eyeball with the iris effect.

Considering that many of us spend time meaninglessly looking at these лоадеры , I want to create a loader effect in which the rotating "eye" looks at the viewer and blinks.

document.getElementById('waitDia').showModal();

var ticks = 300,
    ticker = setInterval(changeTick,1000);

function changeTick()
{
 document.getElementById('spnTick').innerText = --ticks;
 if (0 === ticks) clearInterval(ticker);
}
#waitDia
{
 position:absolute;
 left:0 !important;
 top:0 !important;
 width:100vw !important;
 height:100vh !important; 
 padding:0; 
 min-width:100vw;
 min-height:100vh; 
 background-color:transparent !important;
}

#waitDia::backdrop{background-color:rgba(127,127,127,0.2);}

#spnTick
{
 position:absolute;
 display:inline-block;
 width:100%;
 left:0;
 top:0;
} 
#waitbox
{
 left:0 !important;
 top:0 !important;
 width:100vw !important;
 height:100vh !important;
 position:absolute;
 overflow:hidden;
}


#eyeball
{
 position:relative;
 top:-10vh;
 left:-6px;
 width:calc(24vh + 12px);
 height:calc(24vh + 12px);
 box-sizing:border-box;
 background:rgba(0,128,128,0.5);
 border-radius:100%;
 border:1px solid transparent;
 box-shadow:inset 0 0 18px 2px blue;
 z-index:99999998;
}


#waitsecs
{
 position:absolute;
 left:calc(50vw - 12vh);
 top:46vh;
 width:24vh;
 height:24vh;
 font-size:8vh;
 text-align:center;
 display:block;
 
}

#waitEye
{
 position:absolute;
 top:27vh;
 left:calc(50vw - 23vh);
 width: 46vh;
 height: 46vh;
 background-color: rgba(255,255,255,.9);
 border-radius: 100% 0px;
 transform: rotate(45deg); 
 mix-blend-mode:overlay;
 z-index:199999999;
 box-shadow:0 -0.5vh 0 2px #f1c27d,inset 0 6px 4px 4px black;
}
body,html
{
 background:black;
 font-family:arial;
}
<dialog id='waitDia' class='waitdia'>
   <div id='waitbox'>
    <div id='waitsecs'><span id='spnTick'>300</span><div id='eyeball'></div></div>
   <div id='waitEye'></div> 
   </div>  
  </dialog>

What I've been able to achieve so far is shown below – I've set the ticker here for 300 seconds, just for illustration purposes, so that it will keep working for a long time – in a real application, the latency will probably be significantly less.

While this effect is heading in the right direction, it still lacks the eyelid blink effect. I suspect this is easily doable with proper box-shadow manipulation and simple animation.

I would greatly appreciate anyone who could suggest improvements to complete this implementation.

Free translation of the question Creating a CSS blinking eyelid effect from @DroidOS contributor .

Answer:

Move 2 anchor points in the Bezier curve depending on the time:

requestAnimationFrame(draw);

function draw(t) {

  // двигаем зрачок
  circle.setAttribute('cx', Math.sin(t/1000)*2);

  // анимируем градиент
  grad.setAttribute('offset', 40 + Math.sin(t/3000)*20 + '%');

  // сглаживаем время по формуле easeInOutQuint
  t = Math.max(0, Math.sin(t/300));
  t = (t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t)*6-3;
  
  // кривая Безье в зависимости от сглаженного значения времени
  let d = `-7 0C-2 ${t} 2 ${t} 7 0`;
  mask.setAttribute('d', `M-7 -7${d}L7 -7z`);
  eyelid.setAttribute('d', `M${d}`);

  requestAnimationFrame(draw);
}
<svg viewbox="-10 -10 20 20" height="90vh">
  <defs><radialGradient id="g1" cx="50%" cy="50%" r="50%">
      <stop stop-color="black" offset="0%"/>
      <stop id="grad" stop-color="teal" offset="30%"/>
      <stop stop-color="white" offset="100%"/>
  </radialGradient></defs>
  <circle id="circle" r="2.4" stroke="black" fill="url(#g1)" stroke-width="0.2"></circle>
  <path id="mask" stroke="none" fill="white"></path>
  <path id="eyelid" stroke="black" fill="none"></path>
</svg>

let rnd = (a, b) => (a||0) + ((b-a)||1) * Math.random();
let eyes = Array(22).fill(0).map((e, i) => ({
  i, 
  t: rnd(1, 10), 
  x: rnd(-96, 96), 
  y: rnd(-46, 46), 
  k: rnd(0.05,0.2), 
  r: rnd(-33,33)
}))

defs.innerHTML = eyes.map(e => `
  <radialGradient id="gradient_${e.i}" cx="50%" cy="50%" r="50%">
    <stop stop-color="black" offset="0%"/>
    <stop id="color_${e.i}" stop-color="hsl(${rnd(0, 360)},55%,35%)" offset="30%"/>
    <stop stop-color="white" offset="100%"/>
  </radialGradient>
  <clipPath id="clip_${e.i}">
    <path></path>
  </clipPath>
`).join('');

g.innerHTML = eyes.map(e => `
  <g id="eye_${e.i}" transform="rotate(${e.r})scale(${e.k})translate(${e.x},${e.y})">
    <circle r="2.4" stroke="black" fill="url(#gradient_${e.i})" stroke-width="0.2" clip-path="url(#clip_${e.i})"></circle>
    <path stroke="black" fill="none"></path>
  </g>  
`).join('');

eyes.forEach(e => e.elements = {
  circle: document.querySelector(`#eye_${e.i} circle`),
  clipPath: document.querySelector(`#clip_${e.i} path`),
  eyelid: document.querySelector(`#eye_${e.i} path`),
  gradient: document.querySelector(`#color_${e.i}`)
})

requestAnimationFrame(draw);

function draw(t) {

  eyes.forEach(e => {
    e.elements.circle.setAttribute('cx', Math.sin(t/100/(e.t+e.i))*2);
    e.elements.gradient.setAttribute('offset', 40 + Math.sin(t/300/(e.t+e.i))*20 + '%');
    let T = Math.max(0, Math.sin(t/50/(e.t+e.i)));
    T = (T<.5 ? 16*T*T*T*T*T : 1+16*(--T)*T*T*T*T)*6-3;
    let d = `-7 0C-2 ${T} 2 ${T} 7 0`;
    e.elements.clipPath.setAttribute('d', `M-7 3${d}L7 3z`);
    e.elements.eyelid.setAttribute('d', `M${d}`);
  })
  

  requestAnimationFrame(draw);
}
<svg viewbox="-20 -10 40 20" height="90vh">
  <defs id="defs"></defs>
  <g id="g"></g>
</svg>

PS: the anti-aliasing function is taken from here: https://gist.github.com/gre/1650294

Scroll to Top