1. 赛博朋克风格的视觉设计
-
颜色与渐变:通过
linear-gradient
设置了背景的颜色渐变,使用高饱和度的霓虹色彩(如橙色、绿色和蓝色)来营造赛博朋克的视觉效果。这种配色方案是赛博朋克风格的典型元素。 -
立体感和阴影:使用
box-shadow
为字符方框添加阴影,使其看起来具有一定的立体感和浮动感,模拟电子设备或键盘按键的效果。 -
文本阴影:通过
text-shadow
给字符添加阴影,增强了赛博朋克风格的霓虹灯效果。这种效果在高对比度的背景下尤其突出,营造出虚拟世界的视觉效果。
2. 动态效果与动画
-
Glitch动画:利用
@keyframes
定义了glitch
动画,通过clip-path
和transform
模拟文本的抖动和错位,营造出电子干扰(glitch)的效果。这种故障效果是赛博朋克风格中常见的表现形式,模拟了数字世界中不稳定的电子信号。 -
伪元素
::after
:使用::after
伪元素在每个字符方框后叠加一个内容相同的元素,通过visibility
控制显示与隐藏,并在鼠标悬停时触发glitch
动画,使其看起来像是字符发生了瞬间故障。
3. 交互与响应
-
鼠标悬停效果:在
.character-box:hover::after
中定义了鼠标悬停时的动画效果,当用户将鼠标悬停在字符方框上时,伪元素::after
显示并触发glitch
效果。这种交互为页面增添了动态元素,使用户的体验更加生动。 -
按键状态变化:通过CSS类的切换(如
.correct
,.incorrect
,.highlighted
)动态更新字符方框的状态和颜色,实时反馈用户输入的正确性。这种视觉反馈让用户能够迅速了解自己输入的正确与否。
4. 布局与排版
-
容器布局:使用
display: inline-block;
和text-align: center;
将字符方框、输入框和结果展示区域合理布局。整个页面通过设置width
和margin
,保持在不同设备和屏幕尺寸上的一致性。 -
字符方框的设计:每个字符被放置在独立的
.character-box
容器中,使得每个字符都有自己的背景、阴影和动画效果。这种设计不仅清晰美观,还增强了赛博朋克风格的整体感。
5. JavaScript 动态逻辑
-
分页显示:通过JavaScript将长文本拆分为每页100个字符,并在用户打完一页后自动切换到下一页,实现了文本的分页显示,防止内容过于拥挤。
-
实时输入检查:JavaScript动态检查用户输入的每个字符,利用
.correct
,.incorrect
,.highlighted
类名的切换实现实时的视觉反馈。
主要代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>中文打字练习</title>
<link href="https://fonts.font.im/css?family=Do+Hyeon" rel="stylesheet">
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
background-image: url('https://bkimg.cdn.bcebos.com/pic/0ff41bd5ad6eddc451da707ff483a1fd5266d11695a4?x-bce-process=image/format,f_auto/quality,Q_70/resize,m_lfit,limit_1,w_536');
background-size: cover;
background-position: center;
background-attachment: fixed;
color: white;
}
#text-to-type-container {
display: inline-block;
font-size: 24px;
margin: 20px 0;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 10px;
text-align: left;
width: 80%;
}
.character-box {
display: inline-block;
width: 40px;
height: 55px;
line-height: 55px;
text-align: center;
margin: 2px;
font-weight: bold;
font-family: 'Do Hyeon', sans-serif;
border-radius: 5px;
background: linear-gradient(30deg,transparent 10%,rgb(255, 136, 0) 10% 95%, rgb(0, 255, 149) 95%);
box-shadow: 5px 0 0 rgb(0, 204, 255);
color: rgb(255, 251, 251);
position: relative;
overflow: hidden;
}
.character-box::after {
content: attr(data-char);
position: absolute;
top: 0;
left: 0;
text-shadow: -5px -2px 0 rgb(0, 183, 255),
5px 2px 0 rgb(0, 255, 115);
visibility: hidden;
width: 100%;
height: 100%;
background: linear-gradient(30deg,transparent 10%,rgb(255, 136, 0) 10% 95%, rgb(0, 255, 149) 95%);
}
.character-box.correct {
background: linear-gradient(30deg,transparent 10%,#34a853 10% 95%, #a8e6cf 95%);
box-shadow: 5px 0 0 #34a853;
}
.character-box.incorrect {
background: linear-gradient(30deg,transparent 10%,#d32f2f 10% 95%, #ff8a80 95%);
box-shadow: 5px 0 0 #d32f2f;
}
.character-box.highlighted {
background: linear-gradient(30deg,transparent 10%,#fbc02d 10% 95%, #fff176 95%);
box-shadow: 5px 0 0 #fbc02d;
}
.character-box:hover::after {
animation: glitch 1s;
animation-timing-function: steps(1, end);
visibility: visible;
}
@keyframes glitch {
0% {
clip-path: inset(20% -5px 60% 0);
transform: translate(-6px, 5px);
}
10% {
clip-path: inset(50% -5px 30% 0);
transform: translate(6px, -5px);
}
20% {
clip-path: inset(20% -5px 60% 0);
transform: translate(5px, 0px);
}
30% {
clip-path: inset(80% -5px 5% 0);
transform: translate(-8px, 5px);
}
40% {
clip-path: inset(0 -5px 80% 0);
transform: translate(-4px, -3px);
}
50% {
clip-path: inset(50% -5px 30% 0);
transform: translate(-6px, -5px);
}
60% {
clip-path: inset(80% -5px 5% 0);
transform: translate(-7px, 5px);
}
70% {
clip-path: inset(0 -5px 80% 0);
transform: translate(3px, 6px);
}
80% {
clip-path: inset(50% -5px 30% 0);
transform: translate(5px, 5px);
}
90% {
clip-path: inset(20% -5px 60% 0);
transform: translate(6px, -5px);
}
100% {
clip-path: inset(0 -5px 80% 0);
transform: translate(1px, 5px);
}
}
#user-input {
width: 80%;
height: 100px;
font-size: 24px;
margin-top: 20px;
border: 2px solid #ccc;
padding: 10px;
outline: none;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 10px;
color: black;
}
#results {
margin-top: 20px;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 10px;
display: inline-block;
}
#pagination {
margin-top: 20px;
}
button {
font-size: 18px;
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #007bff;
color: white;
cursor: pointer;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>中文打字练习</h1>
<div id="text-to-type-container"></div>
<textarea id="user-input" placeholder="在此输入..." oninput="checkTyping()" onblur="checkCompletion()"></textarea>
<div id="results">
<p>打字速度: <span id="speed">0</span> 字/分钟</p>
<p>准确率: <span id="accuracy">100</span>%</p>
<p>用时: <span id="time-taken">0</span> 秒</p>
</div>
<div id="pagination">
<button onclick="previousPage()" disabled id="prev-button">上一页</button>
<button onclick="nextPage()" id="next-button">下一页</button>
</div>
<script>
const textToType = "王楚钦,男,2000年5月11日出生于吉林省吉林市,国际级运动健将 ,中国男子乒乓球运动员。效力于山东魏桥乒乓球俱乐部和中国男子乒乓球队。 2015年12月,升入中国国家乒乓球队一队。2017年12月与薛飞获2017世界青少年锦标赛男双冠军 。2018年7月获2018年韩国乒乓球公开赛混双亚军;8月获2018年雅加达亚运会乒乓球男团冠军 ;10月获得2018布宜诺斯艾利斯青奥会乒乓球男单冠军 。2019年12月9日获“北京青年榜样·时代楷模”人物评选“青少年体育之星”。2021年7月入选2020年东京奥运会中国体育代表团乒乓球项目运动员名单;9月获第十四届全运会男双冠军;2021年11月休斯顿世乒赛混双搭档孙颖莎夺得混双金牌 。2022年1月获WTT澳门冠军赛男子单打冠军;10月获成都第56届世界乒乓球团体锦标赛冠军、WTT澳门冠军赛男子单打冠军 、新乡WTT世界杯男子单打冠军 。2023年4月获2023年WTT冠军赛澳门站男单冠军;9月获杭州第19届亚运会乒乓球男子团体、混双、男单、男双冠军。2024年2月获釜山世乒赛团体赛男子团体决赛冠军 ;5月获2024年WTT沙特阿拉伯大满贯男单、男双、混双冠军 。2024年巴黎奥运会,王楚钦入选中国国家乒乓球队大名单,出战男单、男团以及混双项目。2024年7月获得巴黎奥运会乒乓球混双冠军。";
const textToTypeContainer = document.getElementById('text-to-type-container');
const userInput = document.getElementById('user-input');
const speedDisplay = document.getElementById('speed');
const accuracyDisplay = document.getElementById('accuracy');
const timeTakenDisplay = document.getElementById('time-taken');
const prevButton = document.getElementById('prev-button');
const nextButton = document.getElementById('next-button');
const charsPerPage = 100;
let currentPage = 0;
let totalPages = Math.ceil(textToType.length / charsPerPage);
let startTime = null;
let endTime = null;
let typedCharacters = 0;
function displayText() {
textToTypeContainer.innerHTML = '';
const start = currentPage * charsPerPage;
const end = Math.min(start + charsPerPage, textToType.length);
const pageText = textToType.slice(start, end);
pageText.split('').forEach(char => {
const span = document.createElement('span');
span.innerText = char;
span.classList.add('character-box');
span.setAttribute('data-char', char);
textToTypeContainer.appendChild(span);
});
}
displayText();
function checkTyping() {
const typedText = userInput.value;
if (!startTime) {
startTime = new Date();
}
typedCharacters = typedText.length;
// 计算打字速度 (字/分钟)
const elapsedTime = (new Date() - startTime) / 60000; // 转换为分钟
const speed = Math.round(typedCharacters / elapsedTime);
speedDisplay.innerText = speed;
// 计算准确率
let correctCharacters = 0;
const characters = textToTypeContainer.children;
for (let i = 0; i < characters.length; i++) {
const currentChar = characters[i];
if (i < typedText.length) {
if (typedText[i] === currentChar.innerText) {
currentChar.classList.add('correct');
currentChar.classList.remove('incorrect', 'highlighted');
correctCharacters++;
} else {
currentChar.classList.add('incorrect');
currentChar.classList.remove('correct', 'highlighted');
}
} else if (i < typedCharacters) {
currentChar.classList.add('highlighted');
currentChar.classList.remove('correct', 'incorrect');
} else {
currentChar.classList.remove('correct', 'incorrect', 'highlighted');
}
}
const accuracy = Math.round((correctCharacters / typedCharacters) * 100);
accuracyDisplay.innerText = isNaN(accuracy) ? 100 : accuracy;
// 检查是否完成当前页
if (typedText.length >= characters.length && currentPage < totalPages - 1) {
userInput.value = ''; // 清空输入框
nextPage();
} else if (typedText.length >= characters.length && currentPage === totalPages - 1) {
endTime = new Date();
const totalTimeTaken = ((endTime - startTime) / 1000).toFixed(2); // 以秒为单位
timeTakenDisplay.innerText = totalTimeTaken;
}
}
function nextPage() {
if (currentPage < totalPages - 1) {
currentPage++;
displayText();
prevButton.disabled = false;
if (currentPage === totalPages - 1) {
nextButton.disabled = true;
}
}
}
function previousPage() {
if (currentPage > 0) {
currentPage--;
displayText();
nextButton.disabled = false;
if (currentPage === 0) {
prevButton.disabled = true;
}
}
}
</script>
</body>
</html>