个人小项目——简单的CSS和JavaScript实现Glitch文字效果
实际效果
想要给网站主页的文字加上死亡搁浅主题的、带有Glitch效果和打字机效果的文字。其实这个字样之前在使用WordPress的时候就已经做好了,但是无奈WordPress用的php,一特别老,性能优化差,二兼容性和稳定性实在是不好,bug频出。
代码
@font-face {
font-family: 'DS_ExPsNeon';
src: url('https://cynsm.yukiroki.icu//upload/EX%20PS%20Medium%20Neon.otf') format('opentype');
/* src: url('https://cynsm.yukiroki.icu//upload/EX%20PS%20Medium%20Neon.woff2') format('woff2'),
url('https://cynsm.yukiroki.icu//upload/EX%20PS%20Medium%20Neon.woff') format('woff'),
font-weight: normal;
font-style: normal;
font-display: swap; / 优化加载体验 */
}
.element {
font-family: 'DS_ExPsNeon', 'Consolas', 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 1.5em;
color: #B0C4DE; /* YOO IT'S DEATH STRANDING /
text-shadow:
0 0 2px rgba(176, 196, 222, 0.3),
0 0 5px rgba(70, 130, 180, 0.2);
position: relative; / 伪元素定位 /
display: inline-block; / or block, it's all right */
letter-spacing: 1px;
}
.element .typed-cursor {
color: #FF8C00;
opacity: 1;
font-weight: bold;
animation: ds-blink 0.8s infinite;
}
@keyframes ds-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; } /* crt monitor? */
}
/* --- Glitch --- /
.element::red,
.element::blue {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
overflow: hidden; / 确保错位的部分不会超出原始范围 /
clip-path: inset(0);
}
/ 红色通道 */
.element::red {
left: 1px;
text-shadow: -2px 1px rgba(255, 0, 80, 0.7);
animation: glitch-anim-1 3s infinite linear alternate-reverse;
}
/* 青色通道 */
.element::blue {
left: -1px;
text-shadow: 2px 2px rgba(0, 255, 255, 0.7);
animation: glitch-anim-2 2.5s infinite linear alternate-reverse;
}
.element {
animation: glitch-main-shake 1s infinite linear alternate;
}
@keyframes glitch-anim-1 {
0% { clip-path: inset(30% 0 60% 0); transform: translate(-0.05em, -0.025em); opacity: 0.8; }
5% { clip-path: inset(5% 0 80% 0); transform: translate(0.05em, 0.01em); }
10% { clip-path: inset(75% 0 5% 0); transform: translate(-0.07em, -0.015em); }
15% { clip-path: inset(40% 0 40% 0); transform: translate(0.03em, 0.02em); opacity: 0.6; }
20% { clip-path: inset(90% 0 2% 0); transform: translate(-0.02em, 0.005em); }
25% { clip-path: inset(15% 0 70% 0); transform: translate(0.08em, -0.03em); }
30% { clip-path: inset(60% 0 30% 0); transform: translate(-0.04em, 0.015em); opacity: 0.7; }
50% { clip-path: inset(50% 0 50% 0); transform: translate(0,0); opacity: 1;}
70% { clip-path: inset(10% 0 85% 0); transform: translate(0.06em, -0.02em); }
80% { clip-path: inset(80% 0 10% 0); transform: translate(-0.03em, 0.025em); opacity: 0.85; }
90% { clip-path: inset(25% 0 65% 0); transform: translate(0.04em, -0.01em); }
100% { clip-path: inset(55% 0 35% 0); transform: translate(-0.06em, 0.03em); opacity: 0.9; }
}
@keyframes glitch-anim-2 {
0% { clip-path: inset(80% 0 10% 0); transform: translate(0.04em, 0.02em); opacity: 0.75; }
4% { clip-path: inset(20% 0 70% 0); transform: translate(-0.06em, -0.01em); }
16% { clip-path: inset(65% 0 15% 0); transform: translate(0.02em, 0.025em); opacity: 0.8; }
22% { clip-path: inset(30% 0 50% 0); transform: translate(-0.05em, -0.03em); }
/* ... /
48% { clip-path: inset(50% 0 50% 0); transform: translate(0,0); opacity: 1;}
/ ... */
73% { clip-path: inset(5% 0 90% 0); transform: translate(-0.07em, 0.01em); opacity: 0.65; }
85% { clip-path: inset(70% 0 20% 0); transform: translate(0.03em, -0.015em); }
93% { clip-path: inset(45% 0 45% 0); transform: translate(-0.01em, 0.005em); opacity: 0.9; }
100% { clip-path: inset(10% 0 82% 0); transform: translate(0.05em, -0.025em); opacity: 0.7; }
}
@keyframes glitch-main-shake {
0% { transform: translate(0, 0) skewX(0); opacity: 1; color: #B0C4DE; }
1% { transform: translate(0.8px, -0.3px) skewX(-1.5deg); }
2% { transform: translate(-0.5px, 0.7px) skewX(1deg); opacity: 0.95; }
3% { transform: translate(1.2px, 0.2px) skewX(-2deg); color: #A8BEDC; }
4% { transform: translate(-0.8px, -0.9px) skewX(1.8deg); }
5% { transform: translate(0.4px, 0.6px) skewX(-1.2deg); opacity: 1; }
6% { transform: translate(0, 0) skewX(0); } /* 短暂恢复 */
10% { transform: translate(-1.5px, 1px) skewX(2.5deg); opacity: 0.7; }
10.5% { transform: translate(1px, -1.2px) skewX(-2deg); opacity: 0.4; color: #C8D8E8; }
11% { transform: translate(0, 0) skewX(0); opacity: 1; color: #B0C4DE; }
15% { transform: translate(0.3px, -0.6px) skewX(-0.5deg); }
16% { transform: translate(-0.7px, 0.2px) skewX(0.8deg); }
17% { transform: translate(0.5px, 0.4px) skewX(-0.7deg); opacity: 0.98; }
18% { transform: translate(-0.3px, -0.8px) skewX(0.6deg); }
19% { transform: translate(0.6px, 0.1px) skewX(-0.9deg); }
20% { transform: translate(0, 0) skewX(0); }
30% { transform: translate(2px, -1.5px) skewX(-3deg); opacity: 0.5; color: #90AACE; }
30.3% { transform: translate(-2.5px, 2px) skewX(3.5deg); opacity: 0.2; }
30.6% { transform: translate(0, 0) skewX(0); opacity: 1; color: #B0C4DE; }
31%, 69% { opacity: 1; color: #B0C4DE; /* transform: scale(1.001, 0.999); */ }
70% { transform: translate(-1px, 0.5px) skewX(1.5deg); opacity: 0.9; }
71% { transform: translate(0.7px, -1px) skewX(-1deg); color: #D0E0F0; }
72% { transform: translate(-0.4px, 0.8px) skewX(2deg); }
73% { transform: translate(1.1px, -0.6px) skewX(-1.7deg); opacity: 0.6; }
73.5% { transform: translate(0,0); opacity: 0.1; color: #FF8C00; }
74% { transform: translate(0,0); opacity: 1; color: #B0C4DE; }
85% { transform: translate(0.9px, -0.2px) skewX(-1.3deg); }
86% { transform: translate(-0.6px, 0.7px) skewX(0.9deg); opacity: 0.97; }
87% { transform: translate(1px, 0.3px) skewX(-1.6deg); }
88% { transform: translate(-0.7px, -0.8px) skewX(1.4deg); }
89% { transform: translate(0.2px, 0.5px) skewX(-1.1deg); }
90%, 100% { transform: translate(0, 0) skewX(0); opacity: 1; color: #B0C4DE; }
}document.addEventListener('DOMContentLoaded', function() {
const typedOutputElement = document.querySelector('span.element');
if (typedOutputElement) {
const updateDataTextAttribute = () => {
if (typedOutputElement.innerText) {
typedOutputElement.setAttribute('data-text', typedOutputElement.innerText);
} else {
typedOutputElement.removeAttribute('data-text');
}
};
const observer = new MutationObserver((mutationsList, observer) => {
updateDataTextAttribute();
});
observer.observe(typedOutputElement, {
childList: true,
characterData: true,
subtree: true
});
// 初始调用
setTimeout(updateDataTextAttribute, 300); // 略微延迟以确保Typed.js已初始化
} else {
console.warn('Typed.js output element with class "element" not found for glitch effect.');
}
});{
"strings": [
"Construction Complete",
"Welcome",
"Tomorrow is in your hands"
],
"typeSpeed": 25,
"backSpeed": 30,
"backDelay": 2000,
"startDelay": 500,
"loop": true,
"showCursor": true,
"cursorChar": "_",
"smartBackspace": true,
"contentType": "html"
}代码解释
CSS部分
初始化代码
@font-face {
font-family: 'DS_ExPsNeon';
src: url('https://cynsm.yukiroki.icu//upload/EX%20PS%20Medium%20Neon.otf') format('opentype');
/* src: url('https://cynsm.yukiroki.icu//upload/EX%20PS%20Medium%20Neon.otf') format('woff2'),
url('https://cynsm.yukiroki.icu//upload/EX%20PS%20Medium%20Neon.otf') format('woff'),
font-weight: normal;
font-style: normal;
font-display: swap; / 优化加载体验 */
}@font-face 来引入自定义font-family: DS_ExPsNeon,字体文件来自这条reddit帖子。下面的src:是对应文件url的调用,格式.otf(opentype)
以防万一,我注释了woff2和woff格式,说不定能用上呢
.element基础样式
.element {
font-family: 'DS_ExPsNeon', 'Consolas', 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 1.5em;
color: #B0C4DE; /* YOO IT'S DEATH STRANDING /
text-shadow:
0 0 2px rgba(176, 196, 222, 0.3),
0 0 5px rgba(70, 130, 180, 0.2);
position: relative; / 伪元素定位 /
display: inline-block; / or block, it's all right */
letter-spacing: 1px;
}.element是Halo的这个主题调用主页typed.js字样的方式,F12查看Elements可知。
font-family: 'DS_ExPsNeon', 'Consolas', 'Menlo', 'Monaco', 'Courier New', monospace; 字体栈,降级方案。具体作用就是万一自定义字体加载失败或者不可用的时候依然能够显示等宽字体
颜色不赘述
position: relative 伪元素定位。为后续的::red和::blue伪元素绝对定位打下了基础
display: inline-block inline-block让元素机能像行内元素一样排列,又能根据需要设置宽高
.element光标
.element .typed-cursor {
color: #FF8C00;
opacity: 1;
font-weight: bold;
animation: ds-blink 0.8s infinite;
}
@keyframes ds-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; } /* crt monitor? */
}感觉没啥好说的。唯一要说的就是这个ds-blink,有意模拟的那种老式电视机的感觉
重头戏:Glitch特效
/* --- Glitch --- /
.element::red,
.element::blue {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
overflow: hidden; / 确保错位的部分不会超出原始范围 /
clip-path: inset(0);
}
/ 红色通道 */
.element::red {
left: 1px;
text-shadow: -2px 1px rgba(255, 0, 80, 0.7);
animation: glitch-anim-1 3s infinite linear alternate-reverse;
}
/* 青色通道 */
.element::blue {
left: -1px;
text-shadow: 2px 2px rgba(0, 255, 255, 0.7);
animation: glitch-anim-2 2.5s infinite linear alternate-reverse;
}
.element {
animation: glitch-main-shake 1s infinite linear alternate;
}
@keyframes glitch-anim-1 {
0% { clip-path: inset(30% 0 60% 0); transform: translate(-0.05em, -0.025em); opacity: 0.8; }
5% { clip-path: inset(5% 0 80% 0); transform: translate(0.05em, 0.01em); }
10% { clip-path: inset(75% 0 5% 0); transform: translate(-0.07em, -0.015em); }
15% { clip-path: inset(40% 0 40% 0); transform: translate(0.03em, 0.02em); opacity: 0.6; }
20% { clip-path: inset(90% 0 2% 0); transform: translate(-0.02em, 0.005em); }
25% { clip-path: inset(15% 0 70% 0); transform: translate(0.08em, -0.03em); }
30% { clip-path: inset(60% 0 30% 0); transform: translate(-0.04em, 0.015em); opacity: 0.7; }
50% { clip-path: inset(50% 0 50% 0); transform: translate(0,0); opacity: 1;}
70% { clip-path: inset(10% 0 85% 0); transform: translate(0.06em, -0.02em); }
80% { clip-path: inset(80% 0 10% 0); transform: translate(-0.03em, 0.025em); opacity: 0.85; }
90% { clip-path: inset(25% 0 65% 0); transform: translate(0.04em, -0.01em); }
100% { clip-path: inset(55% 0 35% 0); transform: translate(-0.06em, 0.03em); opacity: 0.9; }
}
@keyframes glitch-anim-2 {
0% { clip-path: inset(80% 0 10% 0); transform: translate(0.04em, 0.02em); opacity: 0.75; }
4% { clip-path: inset(20% 0 70% 0); transform: translate(-0.06em, -0.01em); }
16% { clip-path: inset(65% 0 15% 0); transform: translate(0.02em, 0.025em); opacity: 0.8; }
22% { clip-path: inset(30% 0 50% 0); transform: translate(-0.05em, -0.03em); }
/* ... /
48% { clip-path: inset(50% 0 50% 0); transform: translate(0,0); opacity: 1;}
/ ... */
73% { clip-path: inset(5% 0 90% 0); transform: translate(-0.07em, 0.01em); opacity: 0.65; }
85% { clip-path: inset(70% 0 20% 0); transform: translate(0.03em, -0.015em); }
93% { clip-path: inset(45% 0 45% 0); transform: translate(-0.01em, 0.005em); opacity: 0.9; }
100% { clip-path: inset(10% 0 82% 0); transform: translate(0.05em, -0.025em); opacity: 0.7; }
}
@keyframes glitch-main-shake {
0% { transform: translate(0, 0) skewX(0); opacity: 1; color: #B0C4DE; }
1% { transform: translate(0.8px, -0.3px) skewX(-1.5deg); }
2% { transform: translate(-0.5px, 0.7px) skewX(1deg); opacity: 0.95; }
3% { transform: translate(1.2px, 0.2px) skewX(-2deg); color: #A8BEDC; }
4% { transform: translate(-0.8px, -0.9px) skewX(1.8deg); }
5% { transform: translate(0.4px, 0.6px) skewX(-1.2deg); opacity: 1; }
6% { transform: translate(0, 0) skewX(0); } /* 短暂恢复 */
10% { transform: translate(-1.5px, 1px) skewX(2.5deg); opacity: 0.7; }
10.5% { transform: translate(1px, -1.2px) skewX(-2deg); opacity: 0.4; color: #C8D8E8; }
11% { transform: translate(0, 0) skewX(0); opacity: 1; color: #B0C4DE; }
15% { transform: translate(0.3px, -0.6px) skewX(-0.5deg); }
16% { transform: translate(-0.7px, 0.2px) skewX(0.8deg); }
17% { transform: translate(0.5px, 0.4px) skewX(-0.7deg); opacity: 0.98; }
18% { transform: translate(-0.3px, -0.8px) skewX(0.6deg); }
19% { transform: translate(0.6px, 0.1px) skewX(-0.9deg); }
20% { transform: translate(0, 0) skewX(0); }
30% { transform: translate(2px, -1.5px) skewX(-3deg); opacity: 0.5; color: #90AACE; }
30.3% { transform: translate(-2.5px, 2px) skewX(3.5deg); opacity: 0.2; }
30.6% { transform: translate(0, 0) skewX(0); opacity: 1; color: #B0C4DE; }
31%, 69% { opacity: 1; color: #B0C4DE; /* transform: scale(1.001, 0.999); */ }
70% { transform: translate(-1px, 0.5px) skewX(1.5deg); opacity: 0.9; }
71% { transform: translate(0.7px, -1px) skewX(-1deg); color: #D0E0F0; }
72% { transform: translate(-0.4px, 0.8px) skewX(2deg); }
73% { transform: translate(1.1px, -0.6px) skewX(-1.7deg); opacity: 0.6; }
73.5% { transform: translate(0,0); opacity: 0.1; color: #FF8C00; }
74% { transform: translate(0,0); opacity: 1; color: #B0C4DE; }
85% { transform: translate(0.9px, -0.2px) skewX(-1.3deg); }
86% { transform: translate(-0.6px, 0.7px) skewX(0.9deg); opacity: 0.97; }
87% { transform: translate(1px, 0.3px) skewX(-1.6deg); }
88% { transform: translate(-0.7px, -0.8px) skewX(1.4deg); }
89% { transform: translate(0.2px, 0.5px) skewX(-1.1deg); }
90%, 100% { transform: translate(0, 0) skewX(0); opacity: 1; color: #B0C4DE; }
}::red和::blue是两个伪元素,来创建两个错位的文本层,来实现Glitch特效。为什么呢,一个是红色通道,一个是青色通道,二者相互错位就形成了特有的Glitch效果
content: attr(data-text) 允许伪元素的内容动态地从父元素data-text属性获取,也就是Typed.js改变主元素文本的时候,Glitch通道效果也会同步更新而不会出现奇妙的搞笑情况
position: absolute; top: 0; ..height: 100% 是为了保证伪元素和主元素完美重叠
overflow-hidden 和 clip-path: inset(0) 保证了错位动画不会超出原始文本范围,并且为clip-path动画做好准备
然后就是那一transform: translate 等等实现效果,不再赘述
animation-direction: alternate-reverse 和 alternate: 使动画来回播放
JavaScript部分
document.addEventListener('DOMContentLoaded', function() {
const typedOutputElement = document.querySelector('span.element');
if (typedOutputElement) {
const updateDataTextAttribute = () => {
if (typedOutputElement.innerText) {
typedOutputElement.setAttribute('data-text', typedOutputElement.innerText);
} else {
typedOutputElement.removeAttribute('data-text');
}
};
const observer = new MutationObserver((mutationsList, observer) => {
updateDataTextAttribute();
});
observer.observe(typedOutputElement, {
childList: true,
characterData: true,
subtree: true
});
// 初始调用
setTimeout(updateDataTextAttribute, 300); // 略微延迟以确保Typed.js已初始化
} else {
console.warn('Typed.js output element with class "element" not found for glitch effect.');
}
});DOMContentLoaded 确保整个DOM加载完毕再执行脚本
为什么选择使用 Mutation Observer 而不是用 setInterval ?
setInterval 方法对于这种需要即时响应DOM内容变化的场景是一个很糟糕的选择,且不说编写难度高,就算真的写出来对于性能开销也是很夸张的。因为它不灵活,是盲目的轮询,会按照固定的时间间隔不断执行回调函数,无论DOM是否发生了变化。这意味着什么,这意味着代码在消耗资源不停地读取 innerText 并且尝试更新data-text属性,即便文本99%的时间内都没有改变。完蛋了,服务器资源爆炸了。
除此之外,可能错过更新或延迟、难以与实际变化同步都是这个道理,实际上就是setInterval灵活性不够,而且setInterval写那么一大坨能把人看麻,可读性太差了。
而 Mutation Observer 是事件驱动的,意思是它只在被观察的DOM节点及其子节点真正发生变化的时候才会异步调用回调函数。也就是说,Typed.js实际改变了 span.element 的内容的时候, updateDataTextAttribute 函数才会被执行。没有变化就没有执行,资源节省非常夸张。针对这种DOM实时变化(不变化)的场景,高效精确、针对性强且更简洁现代的API是它的大优势。
JavaScript的异步真是个伟大的发明,尤其是单线程配合事件循环(Event Loop)的模型
setTimeout(updateDataTextAttribute, 300) 确保在typed.js可能已经初始化并且输出一些内容后再执行一次更新
console.warn(...) 纯粹是错误处理,nothing much to say.
Typed.js 初始化
感觉挺简单的没必要说。不过值得一提的是这个smartBackspace在读取到<br>标签的时候就不会继续前删,挺好玩的一个小功能。