더블탭 좋아요

Double-Tap to Like

사진을 두 번 빠르게 탭하면 하트가 터지듯 나타나며 좋아요가 눌리는, 인스타그램이 대중화한 제스처.

다른 이름: Double tap to like
···
html
<div class="dtl" id="dtlcard">
  <div class="photo"></div>
  <div class="heart" id="heart">&#9829;</div>
  <button class="likebtn" id="likebtn">&#9825; <span id="likecount">128</span></button>
</div>
css
.fx-dot{position:absolute;left:0;top:0;width:12px;height:12px;margin:-6px 0 0 -6px;border-radius:50%;
  background:var(--accent);box-shadow:0 0 0 5px color-mix(in srgb,var(--accent) 18%,transparent),0 2px 6px rgba(0,0,0,.3);
  pointer-events:none;z-index:9999;transition:opacity .25s}
.dtl{position:relative;width:min(70%,220px);aspect-ratio:4/3;border-radius:14px;overflow:hidden;
  box-shadow:0 12px 26px rgba(0,0,0,.2)}
.photo{position:absolute;inset:0;background:linear-gradient(135deg,var(--accent-3),var(--accent))}
.heart{position:absolute;left:50%;top:50%;font-size:0;color:#fff;transform:translate(-50%,-50%) scale(.4);
  opacity:0;pointer-events:none;text-shadow:0 4px 14px rgba(0,0,0,.35)}
.heart.pop{font-size:56px;animation:heartpop .7s ease forwards}
@keyframes heartpop{0%{opacity:0;transform:translate(-50%,-50%) scale(.3)}30%{opacity:1;transform:translate(-50%,-50%) scale(1.15)}60%{transform:translate(-50%,-50%) scale(.95)}100%{opacity:0;transform:translate(-50%,-50%) scale(1)}}
.likebtn{position:absolute;left:10px;bottom:10px;padding:6px 10px;border:none;border-radius:999px;
  background:rgba(0,0,0,.35);color:#fff;font-size:clamp(11px,3vmin,13px);font-weight:700;display:flex;gap:6px;align-items:center}
.likebtn.liked{color:#ff5c8a}
js
const fxDot=document.createElement('div');fxDot.className='fx-dot';document.body.appendChild(fxDot);
let fxAuto=true,fxX=innerWidth/2,fxY=innerHeight/2;
function fxHide(){fxDot.style.opacity='0';}
addEventListener('pointerdown',()=>{fxAuto=false;fxHide();},{capture:true});
addEventListener('pointermove',e=>{fxAuto=false;fxHide();fxX=e.clientX;fxY=e.clientY;},{capture:true});
function fxWalk(x,y,ms){
  return new Promise(res=>{
    const sx=fxX,sy=fxY,start=performance.now();
    function step(now){
      if(!fxAuto)return res();
      const p=Math.min(1,(now-start)/ms),e=1-Math.pow(1-p,3);
      fxX=sx+(x-sx)*e;fxY=sy+(y-sy)*e;
      fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
      if(p<1)requestAnimationFrame(step);else res();
    }
    requestAnimationFrame(step);
  });
}
function fxWait(ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){ if(!fxAuto)return res(); if(now-start<ms)requestAnimationFrame(step);else res(); }
    requestAnimationFrame(step);
  });
}

const card=document.getElementById('dtlcard');
const heart=document.getElementById('heart');
const likebtn=document.getElementById('likebtn');
const countEl=document.getElementById('likecount');
let liked=false, count=128;
function doLike(){
  heart.classList.remove('pop'); void heart.offsetWidth; heart.classList.add('pop');
  if(!liked){ liked=true; count++; likebtn.classList.add('liked'); likebtn.firstChild.textContent='♥ '; countEl.textContent=String(count); }
}
function resetLike(){
  liked=false; count=128; likebtn.classList.remove('liked'); likebtn.firstChild.textContent='♡ '; countEl.textContent='128';
}
let lastTap=0;
card.addEventListener('pointerdown',()=>{
  fxAuto=false; fxHide();
  const now=performance.now();
  if(now-lastTap<350) doLike();
  lastTap=now;
});
async function autoSeq(){
  while(fxAuto){
    const r=card.getBoundingClientRect();
    await fxWalk(r.left+r.width/2, r.top+r.height/2, 500); if(!fxAuto) break;
    for(let i=0;i<2 && fxAuto;i++){
      fxDot.style.opacity='0.3'; await fxWait(90);
      fxDot.style.opacity='1'; await fxWait(90);
    }
    if(!fxAuto) break;
    doLike();
    await fxWait(1500); if(!fxAuto) break;
    resetLike();
  }
}
autoSeq();

`pointerdown`마다 현재 시각을 이전 탭 시각과 비교해서, 그 차이가 350ms 안쪽이면 더블탭으로 인정한다. 이 시간 창(window)이 너무 짧으면 반응이 잘 안 되고, 너무 길면 두 번의 별개 탭이 실수로 좋아요로 처리된다.

인식되면 화면 중앙에 하트를 `scale(0.3) → scale(1.15) → scale(1)`로 튕기듯 키우며 `opacity`를 0으로 서서히 낮춰 사라지게 한다(`@keyframes` 한 번으로 처리). 이미 좋아요 상태라면 취소하지 않고 하트 애니메이션만 다시 재생하는 게 SNS 앱들의 공통된 동작이다.

싱글탭(사진 확대 등 다른 동작)과 공존해야 하면, 첫 탭에서 바로 반응하지 말고 350ms를 기다렸다가 "그 안에 두 번째 탭이 없었다"면 싱글탭으로 처리하는 지연 판정이 필요하다.

언제 쓰나

SNS 피드처럼 사진 위 오버레이 형태의 빠른 반응 액션에 씁니다. 발견하기 어려운 제스처이므로 명시적인 좋아요 버튼과 함께 두세요.