復刻調皮的按紐

偶然看到,沒填資料就不給按的按紐,覺得很有趣,就跟著做了一個

Naughty button

HTML的部分很單純: 表單裡面有usernam和password input,以及送出按紐

送出按紐其實有兩個,疉在一起,底下的按紐負賚佔位的任務,上方的會移動

<!-- submit box 包了兩個送出按紐  -->
<div class="form-item submit-box">
  <button type="button" value="placeholder" class="placeholder-btn" disabled></button>
  <button type="submit" value="submit" class="submit-btn" disabled>Submit</button>
</div>

和使用者的互動有三個 (三種事件監聽),依照對象可大致分為兩組:

  • 資料輸入欄位: username、password
    • input event
  • 送出按鈕
    • submit event
    • mouseenter event
// Event Listeners
elements.form.addEventListener('input', handleFormInput);
elements.submitBtn.addEventListener('click', handleFormSubmit);
elements.submitBtn.addEventListener('mouseenter', moveButton);

輸入欄位決定了表單是不是可以送出,表單依據可否送出的狀態,判斷送出按鈕要移動或是送出。可以準備一個變數isReadyToSubmit用來記錄這個會共享的狀態。

輸入欄位監聽: 如果沒填入使用者名稱和密碼,就讓按扭無法點擊 (disabled)

isReadyToSubmit = enteredUsername && enteredPassword;
submitBtn.disabled = !isReadyToSubmit;

比較不同的是動畫: 在滑鼠踫到按鈕時,移動上層的那個按鈕,讓它看起來像有「逃跑」的效果。

需要準備兩個項目:

  • 目標位置
  • 動畫效果

目標位置

以按紐本身長寬為基礎,乘上隨機產生的距離和方向,讓它只會在原本的位置附近 (原來的做法是一路往邊界移動,直到踫到邊再回原位)

const basemovex = 84;
const baseemovey = 36;

const xMove = Math.random() * basemovex * (Math.random() < 0.5 ? -1 : 1)
const yMove = Math.random() * baseemovey * (Math.random() < 0.5 ? -1 : 1)
e.target.style.setProperty('--x-move', `${xMove}px`);
e.target.style.setProperty('--y-move', `${yMove}px`);
e.target.classList.add('moving');

算出來後設定到submit button的property,讓CSS可以用來移到新的位置 (css custom properties)

.submit-btn {
  background-color: #f4a261;
  color: #ffffff;
  border: none;
  cursor: pointer;
  position: absolute;
  left: 0;
  transition: all 0.6s;
  will-change: transform;
  transform: translate(var(--x-move, 0), var(--y-move, 0));
}

移動時像是在逃跑的動畫

一開始就是被像果凍般的動畫吸引。用scale做,看起來就有縮小變大的效果

動畫顯示的條件是:

  • 表單還不能送出時 (缺username或是password)
  • 游標踫到按鈕時 才可以動

JS在符合條件時,在按紐加上moving class,而CSS在發現有.moving class的候播放動畫

// e.target是 submit button
// 監聽submit button mouseenter事件
e.target.classList.add('moving');

CSS custom properties 命名規則: 兩個dash (--) 做為prefix

.submit-btn.moving {
  cursor: not-allowed;
  animation: jellyBounce .6s cubic-bezier(.04,.43,.025,1.07);
  transform: scale(1)
}

@keyframes jellyBounce {
  0%, 100% {
    transform: scale(1) translate(var(--x-move), var(--y-move));
  }
  50% {
    transform: scale(1.2, .8) translate(var(--x-move), var(--y-move));
  }
  80% {
    transform: scale(.9, 1.1) translate(var(--x-move), var(--y-move));
  }
}

如果是採用讓按鈕一直往外「逃跑」的做法,可以用IntersectionObserver監控submit button,讓它在踫到或是靠近邊緣的時候回到原點從新開始

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      // If button is not fully visible and form isn't ready, reset position
      if (!entry.isIntersecting && !isReadyToSubmit) {
        submitBtn.style.setProperty('--x-move', '0px');
        submitBtn.style.setProperty('--y-move', '0px');
        submitBtn.classList.remove('moving');
      }
    });
  },
  {
    root: document.querySelector('.form-container'),
    // Reset when less than 80% of button is visible
    threshold: 0.8,
  }
);

observer.observe(submitBtn);