[前端]HTML模拟实现一个基于摄像头的手势识别交互页面

这个HTML文件实现了一个智能手语识别系统,主要包含以下功能:

  1. 摄像头控制功能

    • 可以开启/关闭摄像头
    • 摄像头控制按钮可以切换状态(开启/关闭)
    • 页面加载时会预先获取摄像头权限(减少首次使用时提示)
  2. 手语识别功能

    • 开始识别按钮启动识别过程
    • 停止识别按钮停止识别
    • 识别结果显示在右侧区域
    • 识别历史记录保存在列表中
  3. 结果复制功能

    • 复制结果按钮可以将当前显示的识别结果复制到剪贴板
    • 复制成功后会显示提示信息
    • 兼容现代浏览器和旧版浏览器的复制方法
  4. 设置功能

    • 设置按钮打开设置弹窗
    • 可以调整识别灵敏度、输出语言和输出格式
    • 设置保存后可以应用到系统中
  5. 用户界面功能

    • 顶部导航栏包含系统名称和功能按钮
    • 左侧视频区域显示摄像头画面
    • 右侧结果区域显示识别结果和历史记录
    • 底部状态栏显示系统状态信息
  6. 其他功能

    • 帮助按钮(目前没有具体实现)
    • 设置弹窗可以关闭(点击X或外部区域)

这个系统主要是一个手语识别的演示界面,包含了基本的UI交互和功能流程,但实际的手语识别算法部分是用模拟数据代替的(代码中的simulateRecognition函数)。

效果展示

代码

代码

html

index.html

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 rel="stylesheet" href="styles.css">
</head>
<body>
    <!-- 顶部导航栏 -->
    <header class="app-header">
        <div class="logo">手语识别助手</div>
        <nav class="main-nav">
            <ul>
                <li><a href="#" class="active">首页</a></li>
                <li><a href="#">设置</a></li>
                <li><a href="#">帮助中心</a></li>
                <li><a href="#">关于我们</a></li>
            </ul>
        </nav>
        <div class="user-controls">
            <button id="settings-btn">设置</button>
            <button id="help-btn">帮助</button>
        </div>
    </header>

    <!-- 主内容区 -->
    <main class="app-content">
        <!-- 左侧视频区域 -->
        <section class="video-section">
            <div class="video-container">
                <video id="capture-video" autoplay muted></video>
                <canvas id="output-canvas" style="display:none;"></canvas>
            </div>
            <div class="video-controls">
                <button id="start-btn">开始识别</button>
                <button id="stop-btn">停止识别</button>
                <button id="camera-toggle-btn">开启摄像头</button>  <!-- 修改为可切换按钮 -->
                <select id="camera-select">
                    <option value="0">默认摄像头</option>
                    <!-- 动态加载摄像头选项 -->
                </select>
            </div>
        </section>

        <!-- 右侧结果区域 -->
        <section class="result-section">
            <h2>识别结果</h2>
            <div class="result-display">
                <div id="current-result" class="current-result">
                    <p>等待识别...</p>
                </div>
                <div id="history-results" class="history-results">
                    <h3>历史记录</h3>
                    <ul id="result-list"></ul>
                </div>
            </div>
            <div class="result-actions">
                <button id="copy-btn">复制结果</button>
                <button id="speak-btn">语音播报</button>
            </div>
        </section>
    </main>

    <!-- 底部状态栏 -->
    <footer class="app-footer">
        <div id="status-indicator" class="status-indicator">
            <span class="status-dot"></span>
            <span id="status-text">系统就绪</span>
        </div>
        <div id="version-info">v1.0.0</div>
    </footer>

    <!-- 设置弹窗 -->
    <div id="settings-modal" class="modal">
        <div class="modal-content">
            <span class="close">&times;</span>
            <h2>系统设置</h2>
            <form id="settings-form">
                <div class="form-group">
                    <label for="sensitivity">识别灵敏度</label>
                    <input type="range" id="sensitivity" min="1" max="10" value="5">
                </div>
                <div class="form-group">
                    <label for="language">输出语言</label>
                    <select id="language">
                        <option value="zh-CN">中文</option>
                        <option value="en-US">英文</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="output-format">输出格式</label>
                    <select id="output-format">
                        <option value="text">纯文本</option>
                        <option value="speech">语音</option>
                        <option value="both">文本+语音</option>
                    </select>
                </div>
                <button type="submit" class="save-btn">保存设置</button>
            </form>
        </div>
    </div>

    <script src="app.js"></script>
</body>
</html>
js

app.js

js 复制代码
document.addEventListener('DOMContentLoaded', function() {
    // 获取DOM元素
    const video = document.getElementById('capture-video');
    const canvas = document.getElementById('output-canvas');
    const startBtn = document.getElementById('start-btn');
    const stopBtn = document.getElementById('stop-btn');
    const cameraToggleBtn = document.getElementById('camera-toggle-btn');
    const cameraSelect = document.getElementById('camera-select');
    const statusText = document.getElementById('status-text');
    const resultDisplay = document.getElementById('current-result');
    const resultList = document.getElementById('result-list');
    const settingsBtn = document.getElementById('settings-btn');
    const settingsModal = document.getElementById('settings-modal');
    const closeBtn = document.querySelector('.close');
    const copyBtn = document.getElementById('copy-btn');  // 获取复制按钮

    let isCameraOn = false;
    let currentStream = null;

    // 页面加载时预先获取摄像头权限(静默模式)
    async function preloadCameraPermission() {
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
                video: { facingMode: 'environment' }
            });
            stream.getTracks().forEach(track => track.stop());
            return true;
        } catch (err) {
            return false;
        }
    }

    // 初始化摄像头(实际使用)
    async function initCamera() {
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
                video: { facingMode: 'environment' }
            });
            video.srcObject = stream;
            currentStream = stream;
            statusText.textContent = '摄像头已就绪';
            isCameraOn = true;
            cameraToggleBtn.textContent = '关闭摄像头';
            return true;
        } catch (err) {
            statusText.textContent = '无法访问摄像头: ' + err.message;
            console.error('摄像头错误:', err);
            return false;
        }
    }

    // 复制结果到剪贴板
    async function copyToClipboard(text) {
        try {
            // 使用现代剪贴板API
            await navigator.clipboard.writeText(text);
            // 显示复制成功提示
            showCopySuccessMessage();
            return true;
        } catch (err) {
            // 如果现代API失败,使用传统方法
            console.error('剪贴板API失败:', err);
            return fallbackCopyToClipboard(text);
        }
    }

    // 传统复制方法(兼容旧浏览器)
    function fallbackCopyToClipboard(text) {
        // 创建临时文本区域
        const textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed';
        textArea.style.left = '-999999px';
        textArea.style.top = '-999999px';
        document.body.appendChild(textArea);

        // 选择文本
        textArea.select();
        textArea.setSelectionRange(0, 99999); // 适用于移动设备

        try {
            // 执行复制命令
            const successful = document.execCommand('copy');
            if (successful) {
                showCopySuccessMessage();
                return true;
            }
        } catch (err) {
            console.error('传统复制方法失败:', err);
        } finally {
            // 移除临时元素
            document.body.removeChild(textArea);
        }
        return false;
    }

    // 显示复制成功提示
    function showCopySuccessMessage() {
        // 创建提示元素
        const message = document.createElement('div');
        message.className = 'copy-success-message';
        message.textContent = '复制成功!';

        // 添加样式
        message.style.position = 'fixed';
        message.style.bottom = '20px';
        message.style.right = '20px';
        message.style.backgroundColor = '#4CAF50';
        message.style.color = 'white';
        message.style.padding = '10px 20px';
        message.style.borderRadius = '4px';
        message.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        message.style.zIndex = '1000';

        // 添加到页面
        document.body.appendChild(message);

        // 3秒后自动消失
        setTimeout(() => {
            message.style.opacity = '0';
            message.style.transition = 'opacity 0.5s ease';
            setTimeout(() => {
                document.body.removeChild(message);
            }, 500);
        }, 3000);
    }

    // 可切换的摄像头控制功能
    cameraToggleBtn.addEventListener('click', async function() {
        if (isCameraOn) {
            if (currentStream) {
                const tracks = currentStream.getTracks();
                tracks.forEach(track => track.stop());
                video.srcObject = null;
                currentStream = null;
                statusText.textContent = '摄像头已关闭';
                isCameraOn = false;
                cameraToggleBtn.textContent = '开启摄像头';
            }
        } else {
            await initCamera();
        }
    });

    // 开始识别
    startBtn.addEventListener('click', async function() {
        if (!video.srcObject) {
            statusText.textContent = '请先开启摄像头';
            return;
        }

        statusText.textContent = '正在识别...';
        // 这里调用实际的识别API
        // 示例中使用模拟数据
        simulateRecognition();
    });

    // 停止识别
    stopBtn.addEventListener('click', function() {
        statusText.textContent = '识别已停止';
        // 实际应用中需要停止识别进程
    });

    // 模拟识别过程
    function simulateRecognition() {
        const sampleResults = [
            '你好',
            '谢谢',
            '再见',
            '请问',
            '多少',
            '时间'
        ];

        let index = 0;
        const interval = setInterval(() => {
            if (index >= sampleResults.length) {
                clearInterval(interval);
                statusText.textContent = '识别完成';
                return;
            }

            const result = sampleResults[index];
            resultDisplay.innerHTML = `<p>${result}</p>`;
            addResultToHistory(result);
            index++;
        }, 2000);
    }

    // 添加结果到历史记录
    function addResultToHistory(result) {
        const li = document.createElement('li');
        li.textContent = result;
        resultList.appendChild(li);
        resultList.scrollTop = resultList.scrollHeight;
    }

    // 复制按钮点击事件
    copyBtn.addEventListener('click', function() {
        // 获取当前显示的结果
        const currentResult = resultDisplay.querySelector('p');
        if (currentResult) {
            const textToCopy = currentResult.textContent;
            copyToClipboard(textToCopy);
        } else {
            alert('没有可复制的内容');
        }
    });

    // 设置按钮点击事件
    settingsBtn.addEventListener('click', function() {
        settingsModal.style.display = 'block';
    });

    // 关闭设置弹窗
    closeBtn.addEventListener('click', function() {
        settingsModal.style.display = 'none';
    });

    // 点击弹窗外部关闭
    window.addEventListener('click', function(event) {
        if (event.target === settingsModal) {
            settingsModal.style.display = 'none';
        }
    });

    // 初始化时默认关闭摄像头
    preloadCameraPermission().then(hasPermission => {
        if (!hasPermission) {
            console.warn('未预先获取权限,首次使用时仍会提示');
        }
    });
});
css

style.css

css 复制代码
/* 基础样式 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: "Microsoft YaHei", sans-serif;
    line-height: 1.6;
    color: #333;
    background-color: #f5f5f5;
}

/* 头部导航 */
.app-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 2rem;
    background-color: #1e88e5;
    color: white;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.logo {
    font-size: 1.5rem;
    font-weight: bold;
}

.main-nav ul {
    display: flex;
    list-style: none;
}

.main-nav li {
    margin: 0 1rem;
}

.main-nav a {
    color: white;
    text-decoration: none;
}

.main-nav a.active {
    font-weight: bold;
    border-bottom: 2px solid white;
}

/* 主内容区 */
.app-content {
    display: flex;
    padding: 2rem;
    gap: 2rem;
}

.video-section, .result-section {
    flex: 1;
    background: white;
    border-radius: 8px;
    padding: 1.5rem;
    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}

.video-container {
    width: 100%;
    height: 400px;
    background: #eee;
    border-radius: 4px;
    overflow: hidden;
    position: relative;
}

#capture-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.video-controls {
    margin-top: 1rem;
    display: flex;
    gap: 0.5rem;
}

.video-controls button {
    padding: 0.5rem 1rem;
    background: #1e88e5;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.video-controls select {
    padding: 0.5rem;
    border-radius: 4px;
    border: 1px solid #ddd;
}

/* 结果区域 */
.result-display {
    margin-top: 1.5rem;
}

.current-result {
    background: #f0f7ff;
    padding: 1rem;
    border-radius: 4px;
    min-height: 100px;
    border-left: 4px solid #1e88e5;
}

.history-results h3 {
    margin-bottom: 0.5rem;
}

#result-list {
    list-style: none;
    max-height: 200px;
    overflow-y: auto;
}

#result-list li {
    padding: 0.5rem;
    border-bottom: 1px solid #eee;
}

.result-actions {
    margin-top: 1.5rem;
    display: flex;
    gap: 0.5rem;
}

.result-actions button {
    padding: 0.5rem 1rem;
    background: #1e88e5;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

/* 底部状态栏 */
.app-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 2rem;
    background-color: #f5f5f5;
    border-top: 1px solid #eee;
}

.status-indicator {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.status-dot {
    width: 10px;
    height: 10px;
    background-color: #4caf50;
    border-radius: 50%;
}

/* 设置弹窗 */
.modal {
    display: none;
    position: fixed;
    z-index: 1000;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.5);
}

.modal-content {
    background-color: white;
    margin: 15% auto;
    padding: 2rem;
    border-radius: 8px;
    width: 50%;
    max-width: 500px;
}

.close {
    float: right;
    font-size: 1.5rem;
    cursor: pointer;
}

.form-group {
    margin-bottom: 1rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.5rem;
}

.form-group input, .form-group select {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.save-btn {
    padding: 0.5rem 1rem;
    background: #1e88e5;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .app-content {
        flex-direction: column;
    }

    .modal-content {
        width: 90%;
        margin: 30% auto;
    }
}


/* 在styles.css文件中添加以下样式 */
.copy-success-message {
    position: fixed;
    bottom: 20px;
    right: 20px;
    background-color: #4CAF50;
    color: white;
    padding: 10px 20px;
    border-radius: 4px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    z-index: 1000;
    opacity: 1;
    transition: opacity 0.5s ease;
}

感兴趣的小伙伴可以在此基础之上丰富实现一下~~~

相关推荐
OpenTiny社区1 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠31 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞35 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js