Cyberpsychosis Wrapper wrapper
The wrapped element glitches out at random, flashing doppelgangers like the eyes of someone going cyberpsycho. (゚Д゚≡゚д゚)!?
Tech Keywords
| Name | Description |
|---|---|
| SVG Filter | SVG filter effects such as blur, distortion, and color adjustment |
| SVG feTurbulence | Generates textures from Perlin noise with separate x and y frequencies, often used as a displacement map |
| SVG feDisplacementMap | Offsets source pixels based on the color values of a displacement map, creating distortion, dent, and ripple effects |
| IntersectionObserver | Detects 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
<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. (́⊙◞౪◟⊙‵)
View example source
<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;
}