Skip to content

Every little fish swimming around is a visitor on this page right now, and one of them is you ( ´ ▽ ` )ノ If they get in the way of reading, feel free to turn them off.

Cyberpsychosis Wrapper wrapper ​

The wrapped element glitches out at random, flashing doppelgangers like the eyes of someone going cyberpsycho. (゚Д゚≡゚д゚)!?

Tech Keywords ​

NameDescription
SVG FilterSVG filter effects such as blur, distortion, and color adjustment
SVG feTurbulenceGenerates textures from Perlin noise with separate x and y frequencies, often used as a displacement map
SVG feDisplacementMapOffsets source pixels based on the color values of a displacement map, creating distortion, dent, and ripple effects
IntersectionObserverDetects when elements enter or leave the viewport

Examples ​

Basic Usage ​

Wrap anything and it glitches on its own. Call glitch() to trigger it instantly. Try hovering the button.

View example source
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>

Form Validation ​

Call glitch() on invalid input and the field glitches out, replacing the usual red-border shake.

Each mistake makes it less stable: intensity keeps rising, and after three strikes it starts glitching on its own. Try typing a bad email on purpose. (́⊙◞౪◟⊙‵)

NIGHT CITY ID // SCANR25
View example source
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>

How It Works ​

An SVG filter copies the element into a few full-color doppelgangers, scatters them around, and stacks them on top. feTurbulence then generates horizontal bands of noise, which feDisplacementMap uses to slice the image ever so slightly.

Each episode lasts a few hundred milliseconds, swapping frames every few dozen milliseconds with the occasional clean frame, so the signal seems to cut in and out. Between episodes the filter is removed entirely, and with "reduce motion" enabled it never glitches.

Source Code ​

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