用HTML5实现实时ASCII艺术摄像头

用HTML5实现实时ASCII艺术摄像头

项目简介

这是一个将摄像头画面实时转换为ASCII字符艺术的Web应用,基于HTML5和原生JavaScript实现。通过本项目可以学习到:

  • 浏览器摄像头API的使用
  • Canvas图像处理技术
  • 实时视频流处理
  • 复杂DOM操作
  • 性能优化技巧

功能亮点

七大特色功能

  1. 多模式字符渲染:支持8种字符集,从经典ASCII到Emoji
  2. 动态调色板:6种预设颜色方案+彩虹渐变效果
  3. 专业级图像处理:亮度/对比度调节、模糊、噪点等特效
  4. 分辨率控制:20-120级精细调节
  5. 实时特效面板:5种视觉特效独立控制
  6. 性能监控:实时显示FPS和分辨率
  7. 数据持久化:支持艺术作品的保存与分享

实现原理

1. 技术架构

摄像头视频流 Canvas图像捕获 图像处理管道 亮度/对比度调整 特效叠加 ASCII转换 DOM实时渲染 交互控制面板

2. 核心算法

像素到ASCII的转换公式

python 复制代码
def pixel_to_ascii(r, g, b, chars):
    brightness = 0.299*r + 0.587*g + 0.114*b  # 感知亮度计算
    index = int(brightness / 255 * (len(chars)-1)) 
    return chars[index]

彩虹色生成算法

javascript 复制代码
function getRainbowColor(offset) {
    const hue = (offset % 360) / 60;
    const i = Math.floor(hue);
    const f = hue - i;
    const p = 0;
    const q = 255 * (1 - f);
    const t = 255 * f;
    
    const rgb = [
        [255, t, p],
        [q, 255, p],
        [p, 255, t],
        [p, q, 255],
        [t, p, 255],
        [255, p, q]
    ][i % 6];
    
    return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
}

关键代码解析

1. 视频流处理

javascript 复制代码
// 获取摄像头访问权限
navigator.mediaDevices.getUserMedia({ video: true })
  .then(stream => {
    const video = document.createElement('video');
    video.srcObject = stream;
    video.autoplay = true;
    
    // 创建双缓冲Canvas
    const mainCanvas = document.createElement('canvas');
    const tempCanvas = document.createElement('canvas');
    
    video.onplaying = () => {
      // 设置动态分辨率
      mainCanvas.width = config.width;
      mainCanvas.height = config.height;
      tempCanvas.width = config.width;
      tempCanvas.height = config.height;
      
      // 启动渲染循环
      requestAnimationFrame(renderFrame);
    };
  });

2. 实时渲染引擎

javascript 复制代码
function renderFrame() {
  // 镜像处理
  if (config.mirror) {
    ctx.save();
    ctx.scale(-1, 1);
    ctx.drawImage(video, -canvas.width, 0);
    ctx.restore();
  } else {
    ctx.drawImage(video, 0, 0);
  }
  
  // 应用图像处理管线
  applyEffectsPipeline();
  
  // 转换ASCII字符
  const imgData = ctx.getImageData(0, 0, width, height);
  let asciiArt = generateAscii(imgData);
  
  // 动态颜色处理
  if (config.color === 'rainbow') {
    asciiArt = applyRainbowEffect(asciiArt);
  }
  
  // DOM更新
  asciiDisplay.textContent = asciiArt;
}

3. 特效系统设计

javascript 复制代码
class EffectPipeline {
  constructor() {
    this.effects = [];
  }

  addEffect(effect) {
    this.effects.push(effect);
  }

  process(imageData) {
    return this.effects.reduce((data, effect) => {
      return effect.apply(data);
    }, imageData);
  }
}

// 示例噪点特效
class NoiseEffect {
  constructor(intensity) {
    this.intensity = intensity;
  }

  apply(imageData) {
    const data = new Uint8Array(imageData.data);
    for (let i = 0; i < data.length; i += 4) {
      const noise = Math.random() * this.intensity * 255;
      data[i] += noise;    // R
      data[i+1] += noise;  // G
      data[i+2] += noise;  // B
    }
    return imageData;
  }
}

参数调节建议

场景 推荐设置
人脸识别 高分辨率 + 标准字符集
艺术创作 低分辨率 + 月亮字符集 + 高噪点
动态捕捉 中分辨率 + 二进制字符 + 高对比度

性能优化

  1. 双缓冲技术:使用临时Canvas避免直接修改源数据
  2. 节流渲染:根据FPS自动调整刷新频率
  3. Web Worker:将图像处理逻辑移至后台线程
  4. 内存复用:重复使用ImageData对象
  5. 惰性计算:仅在参数变化时重新生成字符映射表

扩展方向

🚀 二次开发建议

  • 添加视频滤镜系统
  • 集成语音控制功能
  • 实现WebSocket多人共享视图
  • 开发Chrome扩展版本
  • 添加AR标记识别功能

完整代码实现:

js 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>终极ASCII艺术摄像头</title>
    <style>
        :root {
            --primary-color: #0f0;
            --bg-color: #121212;
            --control-bg: #222;
        }
        
        body {
            background-color: var(--bg-color);
            color: var(--primary-color);
            font-family: 'Courier New', monospace;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            overflow-x: hidden;
            transition: all 0.3s;
        }
        
        header {
            text-align: center;
            margin-bottom: 20px;
            width: 100%;
        }
        
        h1 {
            font-size: 2.5rem;
            margin: 0;
            text-shadow: 0 0 10px currentColor;
            letter-spacing: 3px;
        }
        
        .subtitle {
            font-size: 0.9rem;
            opacity: 0.7;
            margin-top: 5px;
        }
        
        #asciiContainer {
            position: relative;
            margin: 20px 0;
            border: 1px solid var(--primary-color);
            box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
            max-width: 90vw;
            overflow: auto;
        }
        
        #asciiCam {
            font-size: 10px;
            line-height: 10px;
            white-space: pre;
            letter-spacing: 2px;
            text-shadow: 0 0 5px currentColor;
            margin: 0;
            padding: 10px;
            transition: all 0.3s;
        }
        
        .controls {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 15px;
            margin: 20px 0;
            padding: 15px;
            background: var(--control-bg);
            border-radius: 8px;
            max-width: 90vw;
        }
        
        .control-group {
            display: flex;
            flex-direction: column;
            min-width: 150px;
        }
        
        .control-group label {
            margin-bottom: 5px;
            font-size: 0.9rem;
        }
        
        select, button, input {
            background: var(--control-bg);
            color: var(--primary-color);
            border: 1px solid var(--primary-color);
            padding: 8px 12px;
            font-family: inherit;
            border-radius: 4px;
            transition: all 0.3s;
        }
        
        select {
            cursor: pointer;
        }
        
        button {
            cursor: pointer;
            min-width: 100px;
        }
        
        button:hover, select:hover {
            background: var(--primary-color);
            color: #000;
        }
        
        input[type="range"] {
            -webkit-appearance: none;
            height: 5px;
            background: var(--control-bg);
            margin-top: 10px;
        }
        
        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 15px;
            height: 15px;
            background: var(--primary-color);
            border-radius: 50%;
            cursor: pointer;
        }
        
        .stats {
            position: absolute;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.7);
            padding: 5px 10px;
            border-radius: 4px;
            font-size: 0.8rem;
        }
        
        .fullscreen-btn {
            position: absolute;
            top: 10px;
            left: 10px;
            background: rgba(0, 0, 0, 0.7);
            border: none;
            width: 30px;
            height: 30px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            border-radius: 4px;
            font-size: 16px;
        }
        
        .effects-panel {
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: var(--control-bg);
            padding: 20px;
            border-radius: 8px;
            z-index: 100;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
            max-width: 80vw;
        }
        
        .effects-panel.active {
            display: block;
        }
        
        .close-panel {
            position: absolute;
            top: 10px;
            right: 10px;
            background: none;
            border: none;
            color: var(--primary-color);
            font-size: 20px;
            cursor: pointer;
        }
        
        .effect-option {
            margin: 10px 0;
        }
        
        .save-btn {
            background: #4CAF50;
            color: white;
            margin-top: 10px;
        }
        
        footer {
            margin-top: 20px;
            font-size: 0.8rem;
            opacity: 0.7;
            text-align: center;
        }
        
        @media (max-width: 768px) {
            .controls {
                flex-direction: column;
                align-items: center;
            }
            
            h1 {
                font-size: 1.8rem;
            }
        }
    </style>
</head>
<body>
    <header>
        <h1>终极ASCII艺术摄像头</h1>
        <div class="subtitle">实时视频转ASCII艺术 - 高级版</div>
    </header>
    
    <div id="asciiContainer">
        <button class="fullscreen-btn" id="fullscreenBtn">⛶</button>
        <div class="stats" id="stats">
            FPS: 0 | 分辨率: 0x0
        </div>
        <pre id="asciiCam">正在初始化摄像头...</pre>
    </div>
    
    <div class="controls">
        <div class="control-group">
            <label for="charSet">字符集</label>
            <select id="charSet">
                <option value="@%#*+=-:. ">标准</option>
                <option value="01">二进制</option>
                <option value="█▓▒░ ">方块</option>
                <option value="♠♥♦♣♤♡♢♧">扑克</option>
                <option value="☯☮✝☪✡☸✠">符号</option>
                <option value="🌑🌒🌓🌔🌕🌖🌗🌘">月亮</option>
                <option value="▁▂▃▄▅▆▇█">柱状</option>
                <option value="🟥🟧🟨🟩🟦🟪">彩色块</option>
            </select>
        </div>
        
        <div class="control-group">
            <label for="colorScheme">颜色主题</label>
            <select id="colorScheme">
                <option value="#0f0">矩阵绿</option>
                <option value="#f00">霓虹红</option>
                <option value="#0ff">赛博蓝</option>
                <option value="#ff0">荧光黄</option>
                <option value="#f0f">粉紫</option>
                <option value="#fff">纯白</option>
                <option value="rainbow">彩虹</option>
            </select>
        </div>
        
        <div class="control-group">
            <label for="resolution">分辨率</label>
            <input type="range" id="resolution" min="20" max="120" value="60">
            <div id="resolutionValue">60</div>
        </div>
        
        <div class="control-group">
            <label for="brightness">亮度</label>
            <input type="range" id="brightness" min="0" max="200" value="100">
            <div id="brightnessValue">100%</div>
        </div>
        
        <div class="control-group">
            <label for="contrast">对比度</label>
            <input type="range" id="contrast" min="0" max="200" value="100">
            <div id="contrastValue">100%</div>
        </div>
        
        <button id="invertBtn">反色</button>
        <button id="mirrorBtn">镜像</button>
        <button id="pauseBtn">暂停</button>
        <button id="effectsBtn">特效</button>
        <button id="saveBtn" class="save-btn">保存</button>
    </div>
    
    <div class="effects-panel" id="effectsPanel">
        <button class="close-panel" id="closePanel">×</button>
        <h2>特效设置</h2>
        
        <div class="effect-option">
            <label for="effectBlur">模糊效果</label>
            <input type="range" id="effectBlur" min="0" max="10" value="0">
            <div id="effectBlurValue">0</div>
        </div>
        
        <div class="effect-option">
            <label for="effectNoise">噪点强度</label>
            <input type="range" id="effectNoise" min="0" max="100" value="0">
            <div id="effectNoiseValue">0%</div>
        </div>
        
        <div class="effect-option">
            <label for="effectScanlines">扫描线</label>
            <input type="range" id="effectScanlines" min="0" max="100" value="0">
            <div id="effectScanlinesValue">0%</div>
        </div>
        
        <div class="effect-option">
            <label for="effectGlitch">故障效果</label>
            <input type="range" id="effectGlitch" min="0" max="100" value="0">
            <div id="effectGlitchValue">0%</div>
        </div>
        
        <div class="effect-option">
            <label for="effectPixelate">像素化</label>
            <input type="range" id="effectPixelate" min="0" max="100" value="0">
            <div id="effectPixelateValue">0%</div>
        </div>
        
        <button id="applyEffects" class="save-btn">应用特效</button>
    </div>
    
    <footer>
        ASCII艺术摄像头 v2.0 | 使用HTML5和JavaScript构建
    </footer>

    <script>
        // 配置对象
        const config = {
            chars: '@%#*+=-:. ',
            color: '#0f0',
            width: 60,
            height: 40,
            invert: false,
            mirror: true,
            paused: false,
            brightness: 100,
            contrast: 100,
            effects: {
                blur: 0,
                noise: 0,
                scanlines: 0,
                glitch: 0,
                pixelate: 0
            },
            lastTime: 0,
            frameCount: 0,
            fps: 0,
            rainbowOffset: 0
        };
        
        // DOM元素
        const elements = {
            asciiCam: document.getElementById('asciiCam'),
            asciiContainer: document.getElementById('asciiContainer'),
            stats: document.getElementById('stats'),
            fullscreenBtn: document.getElementById('fullscreenBtn'),
            charSet: document.getElementById('charSet'),
            colorScheme: document.getElementById('colorScheme'),
            resolution: document.getElementById('resolution'),
            resolutionValue: document.getElementById('resolutionValue'),
            brightness: document.getElementById('brightness'),
            brightnessValue: document.getElementById('brightnessValue'),
            contrast: document.getElementById('contrast'),
            contrastValue: document.getElementById('contrastValue'),
            invertBtn: document.getElementById('invertBtn'),
            mirrorBtn: document.getElementById('mirrorBtn'),
            pauseBtn: document.getElementById('pauseBtn'),
            effectsBtn: document.getElementById('effectsBtn'),
            saveBtn: document.getElementById('saveBtn'),
            effectsPanel: document.getElementById('effectsPanel'),
            closePanel: document.getElementById('closePanel'),
            effectBlur: document.getElementById('effectBlur'),
            effectBlurValue: document.getElementById('effectBlurValue'),
            effectNoise: document.getElementById('effectNoise'),
            effectNoiseValue: document.getElementById('effectNoiseValue'),
            effectScanlines: document.getElementById('effectScanlines'),
            effectScanlinesValue: document.getElementById('effectScanlinesValue'),
            effectGlitch: document.getElementById('effectGlitch'),
            effectGlitchValue: document.getElementById('effectGlitchValue'),
            effectPixelate: document.getElementById('effectPixelate'),
            effectPixelateValue: document.getElementById('effectPixelateValue'),
            applyEffects: document.getElementById('applyEffects')
        };
        
        // 视频和画布元素
        let video;
        let canvas;
        let ctx;
        let tempCanvas;
        let tempCtx;
        let animationId;
        
        // 初始化函数
        function init() {
            setupEventListeners();
            initCamera();
        }
        
        // 设置事件监听器
        function setupEventListeners() {
            // 控制面板事件
            elements.charSet.addEventListener('change', () => {
                config.chars = elements.charSet.value;
            });
            
            elements.colorScheme.addEventListener('change', () => {
                config.color = elements.colorScheme.value;
                updateColorScheme();
            });
            
            elements.resolution.addEventListener('input', () => {
                const value = elements.resolution.value;
                elements.resolutionValue.textContent = value;
                config.width = value * 1.5;
                config.height = value;
            });
            
            elements.brightness.addEventListener('input', () => {
                const value = elements.brightness.value;
                elements.brightnessValue.textContent = `${value}%`;
                config.brightness = value;
            });
            
            elements.contrast.addEventListener('input', () => {
                const value = elements.contrast.value;
                elements.contrastValue.textContent = `${value}%`;
                config.contrast = value;
            });
            
            elements.invertBtn.addEventListener('click', () => {
                config.invert = !config.invert;
                elements.invertBtn.textContent = config.invert ? '正常' : '反色';
            });
            
            elements.mirrorBtn.addEventListener('click', () => {
                config.mirror = !config.mirror;
                elements.mirrorBtn.textContent = config.mirror ? '镜像' : '原始';
            });
            
            elements.pauseBtn.addEventListener('click', () => {
                config.paused = !config.paused;
                elements.pauseBtn.textContent = config.paused ? '继续' : '暂停';
            });
            
            elements.effectsBtn.addEventListener('click', () => {
                elements.effectsPanel.classList.add('active');
            });
            
            elements.closePanel.addEventListener('click', () => {
                elements.effectsPanel.classList.remove('active');
            });
            
            elements.applyEffects.addEventListener('click', () => {
                elements.effectsPanel.classList.remove('active');
            });
            
            // 特效控制
            elements.effectBlur.addEventListener('input', () => {
                const value = elements.effectBlur.value;
                elements.effectBlurValue.textContent = value;
                config.effects.blur = value;
            });
            
            elements.effectNoise.addEventListener('input', () => {
                const value = elements.effectNoise.value;
                elements.effectNoiseValue.textContent = `${value}%`;
                config.effects.noise = value;
            });
            
            elements.effectScanlines.addEventListener('input', () => {
                const value = elements.effectScanlines.value;
                elements.effectScanlinesValue.textContent = `${value}%`;
                config.effects.scanlines = value;
            });
            
            elements.effectGlitch.addEventListener('input', () => {
                const value = elements.effectGlitch.value;
                elements.effectGlitchValue.textContent = `${value}%`;
                config.effects.glitch = value;
            });
            
            elements.effectPixelate.addEventListener('input', () => {
                const value = elements.effectPixelate.value;
                elements.effectPixelateValue.textContent = `${value}%`;
                config.effects.pixelate = value;
            });
            
            // 全屏按钮
            elements.fullscreenBtn.addEventListener('click', toggleFullscreen);
            
            // 保存按钮
            elements.saveBtn.addEventListener('click', saveAsciiArt);
        }
        
        // 初始化摄像头
        function initCamera() {
            navigator.mediaDevices.getUserMedia({ video: true })
                .then(stream => {
                    video = document.createElement('video');
                    video.srcObject = stream;
                    video.autoplay = true;
                    
                    // 创建主画布
                    canvas = document.createElement('canvas');
                    ctx = canvas.getContext('2d');
                    
                    // 创建临时画布用于特效处理
                    tempCanvas = document.createElement('canvas');
                    tempCtx = tempCanvas.getContext('2d');
                    
                    video.onplaying = startRendering;
                })
                .catch(err => {
                    elements.asciiCam.textContent = `错误: ${err.message}\n请确保已授予摄像头权限`;
                    console.error('摄像头错误:', err);
                });
        }
        
        // 开始渲染
        function startRendering() {
            updateResolution();
            animate();
        }
        
        // 动画循环
        function animate() {
            const now = performance.now();
            config.frameCount++;
            
            // 更新FPS计数
            if (now - config.lastTime >= 1000) {
                config.fps = config.frameCount;
                elements.stats.textContent = `FPS: ${config.fps} | 分辨率: ${config.width}x${config.height}`;
                config.frameCount = 0;
                config.lastTime = now;
            }
            
            // 彩虹效果偏移
            if (config.color === 'rainbow') {
                config.rainbowOffset = (config.rainbowOffset + 1) % 360;
            }
            
            if (!config.paused) {
                renderFrame();
            }
            
            animationId = requestAnimationFrame(animate);
        }
        
        // 渲染帧
        function renderFrame() {
            // 设置画布尺寸
            canvas.width = config.width;
            canvas.height = config.height;
            tempCanvas.width = canvas.width;
            tempCanvas.height = canvas.height;
            
            // 绘制原始视频帧
            if (config.mirror) {
                ctx.save();
                ctx.scale(-1, 1);
                ctx.drawImage(video, -canvas.width, 0, canvas.width, canvas.height);
                ctx.restore();
            } else {
                ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
            }
            
            // 应用图像处理效果
            applyImageEffects();
            
            // 获取像素数据
            const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
            let ascii = '';
            
            // 转换为ASCII
            for (let y = 0; y < canvas.height; y++) {
                for (let x = 0; x < canvas.width; x++) {
                    const i = (y * canvas.width + x) * 4;
                    const r = imgData[i];
                    const g = imgData[i + 1];
                    const b = imgData[i + 2];
                    
                    // 计算亮度 (使用感知亮度公式)
                    let brightness = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
                    
                    // 应用对比度
                    brightness = ((brightness - 0.5) * (config.contrast / 100)) + 0.5;
                    
                    // 应用亮度
                    brightness = brightness * (config.brightness / 100);
                    
                    // 限制在0-1范围内
                    brightness = Math.max(0, Math.min(1, brightness));
                    
                    // 根据亮度选择字符
                    let charIndex = Math.floor(brightness * (config.chars.length - 1));
                    if (config.invert) {
                        charIndex = config.chars.length - 1 - charIndex;
                    }
                    
                    // 确保索引在有效范围内
                    charIndex = Math.max(0, Math.min(config.chars.length - 1, charIndex));
                    
                    ascii += config.chars[charIndex];
                }
                ascii += '\n';
            }
            
            // 更新显示
            elements.asciiCam.textContent = ascii;
        }
        
        // 应用图像处理效果
        function applyImageEffects() {
            // 复制原始图像到临时画布
            tempCtx.drawImage(canvas, 0, 0);
            
            // 应用模糊效果
            if (config.effects.blur > 0) {
                ctx.filter = `blur(${config.effects.blur}px)`;
                ctx.drawImage(tempCanvas, 0, 0);
                ctx.filter = 'none';
            }
            
            // 应用噪点效果
            if (config.effects.noise > 0) {
                const noiseData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                const data = noiseData.data;
                const intensity = config.effects.noise / 100;
                
                for (let i = 0; i < data.length; i += 4) {
                    const noise = (Math.random() - 0.5) * 255 * intensity;
                    data[i] += noise;     // R
                    data[i + 1] += noise; // G
                    data[i + 2] += noise; // B
                }
                
                ctx.putImageData(noiseData, 0, 0);
            }
            
            // 应用扫描线效果
            if (config.effects.scanlines > 0) {
                const intensity = config.effects.scanlines / 100;
                
                for (let y = 0; y < canvas.height; y += 2) {
                    ctx.fillStyle = `rgba(0, 0, 0, ${intensity})`;
                    ctx.fillRect(0, y, canvas.width, 1);
                }
            }
            
            // 应用故障效果
            if (config.effects.glitch > 0 && Math.random() < config.effects.glitch / 100) {
                const glitchAmount = Math.floor(Math.random() * 10 * (config.effects.glitch / 100));
                const glitchData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                const tempData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                
                // 水平偏移
                for (let y = 0; y < canvas.height; y++) {
                    const offset = Math.floor(Math.random() * glitchAmount * 2) - glitchAmount;
                    if (offset !== 0) {
                        for (let x = 0; x < canvas.width; x++) {
                            const srcX = Math.max(0, Math.min(canvas.width - 1, x + offset));
                            const srcPos = (y * canvas.width + srcX) * 4;
                            const dstPos = (y * canvas.width + x) * 4;
                            
                            tempData.data[dstPos] = glitchData.data[srcPos];
                            tempData.data[dstPos + 1] = glitchData.data[srcPos + 1];
                            tempData.data[dstPos + 2] = glitchData.data[srcPos + 2];
                        }
                    }
                }
                
                ctx.putImageData(tempData, 0, 0);
            }
            
            // 应用像素化效果
            if (config.effects.pixelate > 0) {
                const size = Math.max(1, Math.floor(config.effects.pixelate / 10));
                
                if (size > 1) {
                    const smallWidth = Math.floor(canvas.width / size);
                    const smallHeight = Math.floor(canvas.height / size);
                    
                    tempCtx.drawImage(canvas, 0, 0, smallWidth, smallHeight);
                    ctx.imageSmoothingEnabled = false;
                    ctx.drawImage(tempCanvas, 0, 0, smallWidth, smallHeight, 0, 0, canvas.width, canvas.height);
                    ctx.imageSmoothingEnabled = true;
                }
            }
        }
        
        // 更新分辨率
        function updateResolution() {
            const value = elements.resolution.value;
            elements.resolutionValue.textContent = value;
            config.width = value * 1.5;
            config.height = value;
        }
        
        // 更新颜色方案
        function updateColorScheme() {
            if (config.color === 'rainbow') {
                // 彩虹色不需要更新样式,因为它在动画循环中处理
                return;
            }
            
            document.documentElement.style.setProperty('--primary-color', config.color);
            elements.asciiCam.style.color = config.color;
        }
        
        // 切换全屏
        function toggleFullscreen() {
            if (!document.fullscreenElement) {
                elements.asciiContainer.requestFullscreen().catch(err => {
                    console.error('全屏错误:', err);
                });
            } else {
                document.exitFullscreen();
            }
        }
        
        // 保存ASCII艺术
        function saveAsciiArt() {
            const blob = new Blob([elements.asciiCam.textContent], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `ascii-art-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.txt`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
        
        // 启动应用
        init();
        
        // 清理资源
        window.addEventListener('beforeunload', () => {
            if (animationId) cancelAnimationFrame(animationId);
            if (video && video.srcObject) {
                video.srcObject.getTracks().forEach(track => track.stop());
            }
        });
    </script>
</body>
</html>
相关推荐
全宇宙最最帅气的哆啦A梦小怪兽8 分钟前
【Axure结合Echarts绘制图表】
前端·javascript·echarts·产品经理·原型
北京地铁1号线1 小时前
MMdetection推理验证输出详解(单张图片demo)
前端·算法
听闻风很好吃2 小时前
DAY07:Vue Router深度解析与多页面博客系统实战
前端·javascript·vue.js
沙滩小岛小木屋2 小时前
多个vue2工程共享node_modules
开发语言·前端·javascript
亦世凡华、2 小时前
React--》掌握react组件库设计与架构规划
前端·经验分享·react.js·前端框架
菥菥爱嘻嘻2 小时前
JS手写代码篇---手写promise.all
开发语言·前端·javascript
Magnum Lehar2 小时前
vulkan游戏引擎的vulkan/shaders下的image实现
java·前端·游戏引擎
^Rocky3 小时前
vue + ant-design + xlsx 实现表格导出进度提示功能
前端·javascript·vue.js
患得患失9493 小时前
【前端】【React】React性能优化系统总结
前端·react.js·性能优化
LaughingZhu3 小时前
PH热榜 | 2025-05-24
前端·人工智能·经验分享·搜索引擎·产品运营