Skip to content

每條游來游去的小魚,都是正在瀏覽這一頁的訪客,有一條是你自己 ( ´ ▽ ` )ノ 覺得干擾閱讀的話,可以把小魚關掉。

神機錯亂包裝器 wrapper ​

包起來的元素會降低人性損耗,出現神機錯亂徵兆

技術關鍵字 ​

名稱描述
SVG FilterSVG 濾鏡效果,例如:模糊、扭曲、顏色調整等等
SVG feTurbulence以 Perlin 雜訊產生紋理,可分別指定 x、y 方向的頻率,常拿來當位移圖
SVG feDisplacementMap依位移圖的色值偏移取樣來源像素,可做出扭曲、凹陷、波動等效果
IntersectionObserver偵測元素是否進入或離開視窗

使用範例 ​

基本用法 ​

包起來就會自己發作,也可以呼叫 glitch() 立即觸發。滑鼠移到按鈕上試試。

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center py-16">
    <wrapper-cyberpsychosis ref="wrapperRef">
      <base-btn
        :label="t('button')"
        @pointerenter="wrapperRef?.glitch()"
      />
    </wrapper-cyberpsychosis>
  </div>
</template>

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseBtn from '../../base-btn.vue'
import WrapperCyberpsychosis from '../wrapper-cyberpsychosis.vue'

const { t } = useI18n()

const wrapperRef = useTemplateRef('wrapperRef')
</script>

表單驗證 ​

輸入錯誤時呼叫 glitch(),錯越多次越不穩定。

NIGHT CITY ID // SCANR25
查看範例原始碼
vue
<template>
  <div class="w-full flex justify-center py-12">
    <div
      class="cyber-panel"
      :class="{
        'is-error': errorCount > 0,
        'is-success': succeeded,
      }"
    >
      <div class="panel-header">
        <span>NIGHT CITY ID // SCAN</span>
        <span class="panel-tag">R25</span>
      </div>

      <label
        class="field-label"
        :for="inputId"
      >
        EMAIL_ADDRESS
      </label>

      <wrapper-cyberpsychosis
        ref="inputWrapperRef"
        :intensity="intensity"
        :enabled="unstable"
        :interval="[800, 2000]"
      >
        <div class="input-frame">
          <!--
            Chrome 會忽略 autocomplete="off",改給它不認得的值才不會自動填入;
            data-* 分別讓 1Password、LastPass、Bitwarden 跳過這個欄位
          -->
          <input
            :id="inputId"
            v-model="email"
            class="cyber-input"
            placeholder="[email protected]"
            autocomplete="cyber-id-scan"
            data-1p-ignore
            data-lpignore="true"
            data-bwignore
            @input="succeeded = false"
            @keydown.enter="submit"
          >
        </div>
      </wrapper-cyberpsychosis>

      <div class="status">
        <template v-if="message">
          {{ succeeded ? '✓ ACCESS_GRANTED ::' : `▲ ERR_0${Math.min(errorCount, 9)} ::` }}
          {{ message }}
        </template>
      </div>

      <button
        class="cyber-button"
        type="button"
        @click="submit"
      >
        {{ t('submit') }}
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref, useId, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import WrapperCyberpsychosis from '../wrapper-cyberpsychosis.vue'

const { t } = useI18n()

const EMAIL_PATTERN = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
/** errorList 的句數,錯超過就停在最後一句 */
const ERROR_MESSAGE_COUNT = 5

const inputId = useId()
const inputWrapperRef = useTemplateRef('inputWrapperRef')

const email = ref('')
const errorCount = ref(0)
const succeeded = ref(false)

/** 錯越多次越不穩定 */
const intensity = computed(() => Math.min(1 + errorCount.value * 0.3, 2))
/** 錯三次後就開始三不五時自己閃現 */
const unstable = computed(() => errorCount.value >= 3)

const message = computed(() => {
  if (succeeded.value) {
    return t('success')
  }
  if (errorCount.value === 0) {
    return ''
  }

  const index = Math.min(errorCount.value, ERROR_MESSAGE_COUNT) - 1
  return t(`errorList[${index}]`)
})

function submit() {
  if (EMAIL_PATTERN.test(email.value)) {
    errorCount.value = 0
    succeeded.value = true
    return
  }

  errorCount.value++
  inputWrapperRef.value?.glitch(600)
}
</script>

原理 ​

用 SVG 濾鏡把元素複製成幾個原色分身,往四周錯開、疊在本體上;再以 feTurbulence 產生橫條雜訊,交給 feDisplacementMap 做出輕微的切片錯位。

每次發作數百毫秒,每隔幾十毫秒換一格,偶爾恢復正常,訊號才有斷斷續續的感覺。沒發作時直接拿掉濾鏡,系統開啟「減少動態效果」時不會發作。

原始碼 ​

API ​

Props ​

interface Props {
  /**
   * 是否自動隨機發作。關閉後仍可呼叫 `glitch()` 手動觸發。
   *
   * @default true
   */
  enabled?: boolean;
  /**
   * 強度,影響分身間距與切片位移量
   *
   * @default 1
   */
  intensity?: number;
  /**
   * 兩次發作的間隔範圍(ms)
   *
   * @default [1500, 4500]
   */
  interval?: [number, number];
  /**
   * 每次發作的持續時間範圍(ms)
   *
   * @default [200, 700]
   */
  duration?: [number, number];
}

Methods ​

interface Expose {
  /**
   * 立即發作一次
   *
   * @param duration 持續時間(ms),預設從 `duration` 範圍隨機
   */
  glitch: (duration?: number) => void;
}

v0.94.1