用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>
相关推荐
weixin_382395232 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__2 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.2 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~3 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华4 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子6 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅6 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni7 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学8 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端