HTML - 简易版打字练习

1. 赛博朋克风格的视觉设计

  • 颜色与渐变 :通过linear-gradient设置了背景的颜色渐变,使用高饱和度的霓虹色彩(如橙色、绿色和蓝色)来营造赛博朋克的视觉效果。这种配色方案是赛博朋克风格的典型元素。

  • 立体感和阴影 :使用 box-shadow 为字符方框添加阴影,使其看起来具有一定的立体感和浮动感,模拟电子设备或键盘按键的效果。

  • 文本阴影 :通过 text-shadow 给字符添加阴影,增强了赛博朋克风格的霓虹灯效果。这种效果在高对比度的背景下尤其突出,营造出虚拟世界的视觉效果。

2. 动态效果与动画

  • Glitch动画 :利用 @keyframes 定义了 glitch 动画,通过clip-pathtransform模拟文本的抖动和错位,营造出电子干扰(glitch)的效果。这种故障效果是赛博朋克风格中常见的表现形式,模拟了数字世界中不稳定的电子信号。

  • 伪元素 ::after :使用 ::after 伪元素在每个字符方框后叠加一个内容相同的元素,通过visibility控制显示与隐藏,并在鼠标悬停时触发 glitch 动画,使其看起来像是字符发生了瞬间故障。

3. 交互与响应

  • 鼠标悬停效果 :在 .character-box:hover::after 中定义了鼠标悬停时的动画效果,当用户将鼠标悬停在字符方框上时,伪元素 ::after 显示并触发 glitch 效果。这种交互为页面增添了动态元素,使用户的体验更加生动。

  • 按键状态变化 :通过CSS类的切换(如 .correct, .incorrect, .highlighted)动态更新字符方框的状态和颜色,实时反馈用户输入的正确性。这种视觉反馈让用户能够迅速了解自己输入的正确与否。

4. 布局与排版

  • 容器布局 :使用 display: inline-block;text-align: center; 将字符方框、输入框和结果展示区域合理布局。整个页面通过设置 widthmargin,保持在不同设备和屏幕尺寸上的一致性。

  • 字符方框的设计 :每个字符被放置在独立的 .character-box 容器中,使得每个字符都有自己的背景、阴影和动画效果。这种设计不仅清晰美观,还增强了赛博朋克风格的整体感。

5. JavaScript 动态逻辑

  • 分页显示:通过JavaScript将长文本拆分为每页100个字符,并在用户打完一页后自动切换到下一页,实现了文本的分页显示,防止内容过于拥挤。

  • 实时输入检查 :JavaScript动态检查用户输入的每个字符,利用 .correct, .incorrect, .highlighted 类名的切换实现实时的视觉反馈。

主要代码:

html 复制代码
<!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>
相关推荐
活宝小娜42 分钟前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点1 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow1 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o1 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic2 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā2 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年3 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder3 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727573 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
SoaringHeart4 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter