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

[复制链接]
发表于 2025-7-30 19:12:31 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

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

项目简介

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


  • 欣赏器摄像头API的利用
  • Canvas图像处理技能
  • 及时视频流处理
  • 复杂DOM操作
  • 性能优化技巧
功能亮点

七大特色功能

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

1. 技能架构

   2. 核心算法

像素到ASCII的转换公式
  1. def pixel_to_ascii(r, g, b, chars):
  2.     brightness = 0.299*r + 0.587*g + 0.114*b  # 感知亮度计算
  3.     index = int(brightness / 255 * (len(chars)-1))
  4.     return chars[index]
复制代码
彩虹色天生算法
  1. function getRainbowColor(offset) {
  2.     const hue = (offset % 360) / 60;
  3.     const i = Math.floor(hue);
  4.     const f = hue - i;
  5.     const p = 0;
  6.     const q = 255 * (1 - f);
  7.     const t = 255 * f;
  8.    
  9.     const rgb = [
  10.         [255, t, p],
  11.         [q, 255, p],
  12.         [p, 255, t],
  13.         [p, q, 255],
  14.         [t, p, 255],
  15.         [255, p, q]
  16.     ][i % 6];
  17.    
  18.     return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
  19. }
复制代码
关键代码剖析

1. 视频流处理

  1. // 获取摄像头访问权限
  2. navigator.mediaDevices.getUserMedia({ video: true })
  3.   .then(stream => {
  4.     const video = document.createElement('video');
  5.     video.srcObject = stream;
  6.     video.autoplay = true;
  7.    
  8.     // 创建双缓冲Canvas
  9.     const mainCanvas = document.createElement('canvas');
  10.     const tempCanvas = document.createElement('canvas');
  11.    
  12.     video.onplaying = () => {
  13.       // 设置动态分辨率
  14.       mainCanvas.width = config.width;
  15.       mainCanvas.height = config.height;
  16.       tempCanvas.width = config.width;
  17.       tempCanvas.height = config.height;
  18.       
  19.       // 启动渲染循环
  20.       requestAnimationFrame(renderFrame);
  21.     };
  22.   });
复制代码
2. 及时渲染引擎

  1. function renderFrame() {
  2.   // 镜像处理
  3.   if (config.mirror) {
  4.     ctx.save();
  5.     ctx.scale(-1, 1);
  6.     ctx.drawImage(video, -canvas.width, 0);
  7.     ctx.restore();
  8.   } else {
  9.     ctx.drawImage(video, 0, 0);
  10.   }
  11.   
  12.   // 应用图像处理管线
  13.   applyEffectsPipeline();
  14.   
  15.   // 转换ASCII字符
  16.   const imgData = ctx.getImageData(0, 0, width, height);
  17.   let asciiArt = generateAscii(imgData);
  18.   
  19.   // 动态颜色处理
  20.   if (config.color === 'rainbow') {
  21.     asciiArt = applyRainbowEffect(asciiArt);
  22.   }
  23.   
  24.   // DOM更新
  25.   asciiDisplay.textContent = asciiArt;
  26. }
复制代码
3. 特效系统计划

  1. class EffectPipeline {
  2.   constructor() {
  3.     this.effects = [];
  4.   }
  5.   addEffect(effect) {
  6.     this.effects.push(effect);
  7.   }
  8.   process(imageData) {
  9.     return this.effects.reduce((data, effect) => {
  10.       return effect.apply(data);
  11.     }, imageData);
  12.   }
  13. }
  14. // 示例噪点特效
  15. class NoiseEffect {
  16.   constructor(intensity) {
  17.     this.intensity = intensity;
  18.   }
  19.   apply(imageData) {
  20.     const data = new Uint8Array(imageData.data);
  21.     for (let i = 0; i < data.length; i += 4) {
  22.       const noise = Math.random() * this.intensity * 255;
  23.       data[i] += noise;    // R
  24.       data[i+1] += noise;  // G
  25.       data[i+2] += noise;  // B
  26.     }
  27.     return imageData;
  28.   }
  29. }
复制代码
参数调节发起
场景推荐设置人脸识别高分辨率 + 标准字符集艺术创作低分辨率 + 月亮字符集 + 高噪点动态捕获中分辨率 + 二进制字符 + 高对比度性能优化


  • 双缓冲技能:利用临时Canvas避免直接修改源数据
  • 节流渲染:根据FPS主动调整革新频率
  • Web Worker:将图像处理逻辑移至后台线程
  • 内存复用:重复利用ImageData对象
  • 惰性盘算:仅在参数变革时重新天生字符映射表
扩展方向

🚀 二次开发发起


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

完备代码实现:
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>终极ASCII艺术摄像头</title>
  7.     <style>
  8.         :root {
  9.             --primary-color: #0f0;
  10.             --bg-color: #121212;
  11.             --control-bg: #222;
  12.         }
  13.         
  14.         body {
  15.             background-color: var(--bg-color);
  16.             color: var(--primary-color);
  17.             font-family: 'Courier New', monospace;
  18.             display: flex;
  19.             flex-direction: column;
  20.             justify-content: center;
  21.             align-items: center;
  22.             min-height: 100vh;
  23.             margin: 0;
  24.             overflow-x: hidden;
  25.             transition: all 0.3s;
  26.         }
  27.         
  28.         header {
  29.             text-align: center;
  30.             margin-bottom: 20px;
  31.             width: 100%;
  32.         }
  33.         
  34.         h1 {
  35.             font-size: 2.5rem;
  36.             margin: 0;
  37.             text-shadow: 0 0 10px currentColor;
  38.             letter-spacing: 3px;
  39.         }
  40.         
  41.         .subtitle {
  42.             font-size: 0.9rem;
  43.             opacity: 0.7;
  44.             margin-top: 5px;
  45.         }
  46.         
  47.         #asciiContainer {
  48.             position: relative;
  49.             margin: 20px 0;
  50.             border: 1px solid var(--primary-color);
  51.             box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
  52.             max-width: 90vw;
  53.             overflow: auto;
  54.         }
  55.         
  56.         #asciiCam {
  57.             font-size: 10px;
  58.             line-height: 10px;
  59.             white-space: pre;
  60.             letter-spacing: 2px;
  61.             text-shadow: 0 0 5px currentColor;
  62.             margin: 0;
  63.             padding: 10px;
  64.             transition: all 0.3s;
  65.         }
  66.         
  67.         .controls {
  68.             display: flex;
  69.             flex-wrap: wrap;
  70.             justify-content: center;
  71.             gap: 15px;
  72.             margin: 20px 0;
  73.             padding: 15px;
  74.             background: var(--control-bg);
  75.             border-radius: 8px;
  76.             max-width: 90vw;
  77.         }
  78.         
  79.         .control-group {
  80.             display: flex;
  81.             flex-direction: column;
  82.             min-width: 150px;
  83.         }
  84.         
  85.         .control-group label {
  86.             margin-bottom: 5px;
  87.             font-size: 0.9rem;
  88.         }
  89.         
  90.         select, button, input {
  91.             background: var(--control-bg);
  92.             color: var(--primary-color);
  93.             border: 1px solid var(--primary-color);
  94.             padding: 8px 12px;
  95.             font-family: inherit;
  96.             border-radius: 4px;
  97.             transition: all 0.3s;
  98.         }
  99.         
  100.         select {
  101.             cursor: pointer;
  102.         }
  103.         
  104.         button {
  105.             cursor: pointer;
  106.             min-width: 100px;
  107.         }
  108.         
  109.         button:hover, select:hover {
  110.             background: var(--primary-color);
  111.             color: #000;
  112.         }
  113.         
  114.         input[type="range"] {
  115.             -webkit-appearance: none;
  116.             height: 5px;
  117.             background: var(--control-bg);
  118.             margin-top: 10px;
  119.         }
  120.         
  121.         input[type="range"]::-webkit-slider-thumb {
  122.             -webkit-appearance: none;
  123.             width: 15px;
  124.             height: 15px;
  125.             background: var(--primary-color);
  126.             border-radius: 50%;
  127.             cursor: pointer;
  128.         }
  129.         
  130.         .stats {
  131.             position: absolute;
  132.             top: 10px;
  133.             right: 10px;
  134.             background: rgba(0, 0, 0, 0.7);
  135.             padding: 5px 10px;
  136.             border-radius: 4px;
  137.             font-size: 0.8rem;
  138.         }
  139.         
  140.         .fullscreen-btn {
  141.             position: absolute;
  142.             top: 10px;
  143.             left: 10px;
  144.             background: rgba(0, 0, 0, 0.7);
  145.             border: none;
  146.             width: 30px;
  147.             height: 30px;
  148.             display: flex;
  149.             align-items: center;
  150.             justify-content: center;
  151.             cursor: pointer;
  152.             border-radius: 4px;
  153.             font-size: 16px;
  154.         }
  155.         
  156.         .effects-panel {
  157.             display: none;
  158.             position: fixed;
  159.             top: 50%;
  160.             left: 50%;
  161.             transform: translate(-50%, -50%);
  162.             background: var(--control-bg);
  163.             padding: 20px;
  164.             border-radius: 8px;
  165.             z-index: 100;
  166.             box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
  167.             max-width: 80vw;
  168.         }
  169.         
  170.         .effects-panel.active {
  171.             display: block;
  172.         }
  173.         
  174.         .close-panel {
  175.             position: absolute;
  176.             top: 10px;
  177.             right: 10px;
  178.             background: none;
  179.             border: none;
  180.             color: var(--primary-color);
  181.             font-size: 20px;
  182.             cursor: pointer;
  183.         }
  184.         
  185.         .effect-option {
  186.             margin: 10px 0;
  187.         }
  188.         
  189.         .save-btn {
  190.             background: #4CAF50;
  191.             color: white;
  192.             margin-top: 10px;
  193.         }
  194.         
  195.         footer {
  196.             margin-top: 20px;
  197.             font-size: 0.8rem;
  198.             opacity: 0.7;
  199.             text-align: center;
  200.         }
  201.         
  202.         @media (max-width: 768px) {
  203.             .controls {
  204.                 flex-direction: column;
  205.                 align-items: center;
  206.             }
  207.             
  208.             h1 {
  209.                 font-size: 1.8rem;
  210.             }
  211.         }
  212.     </style>
  213. </head>
  214. <body>
  215.     <header>
  216.         <h1>终极ASCII艺术摄像头</h1>
  217.         <div class="subtitle">实时视频转ASCII艺术 - 高级版</div>
  218.     </header>
  219.    
  220.     <div id="asciiContainer">
  221.         <button class="fullscreen-btn" id="fullscreenBtn">⛶</button>
  222.         <div class="stats" id="stats">
  223.             FPS: 0 | 分辨率: 0x0
  224.         </div>
  225.         <pre id="asciiCam">正在初始化摄像头...</pre>
  226.     </div>
  227.    
  228.     <div class="controls">
  229.         <div class="control-group">
  230.             <label for="charSet">字符集</label>
  231.             <select id="charSet">
  232.                 <option value="@%#*+=-:. ">标准</option>
  233.                 <option value="01">二进制</option>
  234.                 <option value="█▓▒░ ">方块</option>
  235.                 <option value="♠♥♦♣♤♡♢♧">扑克</option>
  236.                 <option value="☯☮✝☪✡☸✠">符号</option>
  237.                 <option value="🌑🌒🌓🌔🌕🌖🌗🌘">月亮</option>
  238.                 <option value="▁▂▃▄▅▆▇█">柱状</option>
  239.                 <option value="🟥🟧🟨🟩🟦🟪">彩色块</option>
  240.             </select>
  241.         </div>
  242.         
  243.         <div class="control-group">
  244.             <label for="colorScheme">颜色主题</label>
  245.             <select id="colorScheme">
  246.                 <option value="#0f0">矩阵绿</option>
  247.                 <option value="#f00">霓虹红</option>
  248.                 <option value="#0ff">赛博蓝</option>
  249.                 <option value="#ff0">荧光黄</option>
  250.                 <option value="#f0f">粉紫</option>
  251.                 <option value="#fff">纯白</option>
  252.                 <option value="rainbow">彩虹</option>
  253.             </select>
  254.         </div>
  255.         
  256.         <div class="control-group">
  257.             <label for="resolution">分辨率</label>
  258.             <input type="range" id="resolution" min="20" max="120" value="60">
  259.             <div id="resolutionValue">60</div>
  260.         </div>
  261.         
  262.         <div class="control-group">
  263.             <label for="brightness">亮度</label>
  264.             <input type="range" id="brightness" min="0" max="200" value="100">
  265.             <div id="brightnessValue">100%</div>
  266.         </div>
  267.         
  268.         <div class="control-group">
  269.             <label for="contrast">对比度</label>
  270.             <input type="range" id="contrast" min="0" max="200" value="100">
  271.             <div id="contrastValue">100%</div>
  272.         </div>
  273.         
  274.         <button id="invertBtn">反色</button>
  275.         <button id="mirrorBtn">镜像</button>
  276.         <button id="pauseBtn">暂停</button>
  277.         <button id="effectsBtn">特效</button>
  278.         <button id="saveBtn" class="save-btn">保存</button>
  279.     </div>
  280.    
  281.     <div class="effects-panel" id="effectsPanel">
  282.         <button class="close-panel" id="closePanel">×</button>
  283.         <h2>特效设置</h2>
  284.         
  285.         <div class="effect-option">
  286.             <label for="effectBlur">模糊效果</label>
  287.             <input type="range" id="effectBlur" min="0" max="10" value="0">
  288.             <div id="effectBlurValue">0</div>
  289.         </div>
  290.         
  291.         <div class="effect-option">
  292.             <label for="effectNoise">噪点强度</label>
  293.             <input type="range" id="effectNoise" min="0" max="100" value="0">
  294.             <div id="effectNoiseValue">0%</div>
  295.         </div>
  296.         
  297.         <div class="effect-option">
  298.             <label for="effectScanlines">扫描线</label>
  299.             <input type="range" id="effectScanlines" min="0" max="100" value="0">
  300.             <div id="effectScanlinesValue">0%</div>
  301.         </div>
  302.         
  303.         <div class="effect-option">
  304.             <label for="effectGlitch">故障效果</label>
  305.             <input type="range" id="effectGlitch" min="0" max="100" value="0">
  306.             <div id="effectGlitchValue">0%</div>
  307.         </div>
  308.         
  309.         <div class="effect-option">
  310.             <label for="effectPixelate">像素化</label>
  311.             <input type="range" id="effectPixelate" min="0" max="100" value="0">
  312.             <div id="effectPixelateValue">0%</div>
  313.         </div>
  314.         
  315.         <button id="applyEffects" class="save-btn">应用特效</button>
  316.     </div>
  317.    
  318.     <footer>
  319.         ASCII艺术摄像头 v2.0 | 使用HTML5和JavaScript构建
  320.     </footer>
  321.     <script>
  322.         // 配置对象
  323.         const config = {
  324.             chars: '@%#*+=-:. ',
  325.             color: '#0f0',
  326.             width: 60,
  327.             height: 40,
  328.             invert: false,
  329.             mirror: true,
  330.             paused: false,
  331.             brightness: 100,
  332.             contrast: 100,
  333.             effects: {
  334.                 blur: 0,
  335.                 noise: 0,
  336.                 scanlines: 0,
  337.                 glitch: 0,
  338.                 pixelate: 0
  339.             },
  340.             lastTime: 0,
  341.             frameCount: 0,
  342.             fps: 0,
  343.             rainbowOffset: 0
  344.         };
  345.         
  346.         // DOM元素
  347.         const elements = {
  348.             asciiCam: document.getElementById('asciiCam'),
  349.             asciiContainer: document.getElementById('asciiContainer'),
  350.             stats: document.getElementById('stats'),
  351.             fullscreenBtn: document.getElementById('fullscreenBtn'),
  352.             charSet: document.getElementById('charSet'),
  353.             colorScheme: document.getElementById('colorScheme'),
  354.             resolution: document.getElementById('resolution'),
  355.             resolutionValue: document.getElementById('resolutionValue'),
  356.             brightness: document.getElementById('brightness'),
  357.             brightnessValue: document.getElementById('brightnessValue'),
  358.             contrast: document.getElementById('contrast'),
  359.             contrastValue: document.getElementById('contrastValue'),
  360.             invertBtn: document.getElementById('invertBtn'),
  361.             mirrorBtn: document.getElementById('mirrorBtn'),
  362.             pauseBtn: document.getElementById('pauseBtn'),
  363.             effectsBtn: document.getElementById('effectsBtn'),
  364.             saveBtn: document.getElementById('saveBtn'),
  365.             effectsPanel: document.getElementById('effectsPanel'),
  366.             closePanel: document.getElementById('closePanel'),
  367.             effectBlur: document.getElementById('effectBlur'),
  368.             effectBlurValue: document.getElementById('effectBlurValue'),
  369.             effectNoise: document.getElementById('effectNoise'),
  370.             effectNoiseValue: document.getElementById('effectNoiseValue'),
  371.             effectScanlines: document.getElementById('effectScanlines'),
  372.             effectScanlinesValue: document.getElementById('effectScanlinesValue'),
  373.             effectGlitch: document.getElementById('effectGlitch'),
  374.             effectGlitchValue: document.getElementById('effectGlitchValue'),
  375.             effectPixelate: document.getElementById('effectPixelate'),
  376.             effectPixelateValue: document.getElementById('effectPixelateValue'),
  377.             applyEffects: document.getElementById('applyEffects')
  378.         };
  379.         
  380.         // 视频和画布元素
  381.         let video;
  382.         let canvas;
  383.         let ctx;
  384.         let tempCanvas;
  385.         let tempCtx;
  386.         let animationId;
  387.         
  388.         // 初始化函数
  389.         function init() {
  390.             setupEventListeners();
  391.             initCamera();
  392.         }
  393.         
  394.         // 设置事件监听器
  395.         function setupEventListeners() {
  396.             // 控制面板事件
  397.             elements.charSet.addEventListener('change', () => {
  398.                 config.chars = elements.charSet.value;
  399.             });
  400.             
  401.             elements.colorScheme.addEventListener('change', () => {
  402.                 config.color = elements.colorScheme.value;
  403.                 updateColorScheme();
  404.             });
  405.             
  406.             elements.resolution.addEventListener('input', () => {
  407.                 const value = elements.resolution.value;
  408.                 elements.resolutionValue.textContent = value;
  409.                 config.width = value * 1.5;
  410.                 config.height = value;
  411.             });
  412.             
  413.             elements.brightness.addEventListener('input', () => {
  414.                 const value = elements.brightness.value;
  415.                 elements.brightnessValue.textContent = `${value}%`;
  416.                 config.brightness = value;
  417.             });
  418.             
  419.             elements.contrast.addEventListener('input', () => {
  420.                 const value = elements.contrast.value;
  421.                 elements.contrastValue.textContent = `${value}%`;
  422.                 config.contrast = value;
  423.             });
  424.             
  425.             elements.invertBtn.addEventListener('click', () => {
  426.                 config.invert = !config.invert;
  427.                 elements.invertBtn.textContent = config.invert ? '正常' : '反色';
  428.             });
  429.             
  430.             elements.mirrorBtn.addEventListener('click', () => {
  431.                 config.mirror = !config.mirror;
  432.                 elements.mirrorBtn.textContent = config.mirror ? '镜像' : '原始';
  433.             });
  434.             
  435.             elements.pauseBtn.addEventListener('click', () => {
  436.                 config.paused = !config.paused;
  437.                 elements.pauseBtn.textContent = config.paused ? '继续' : '暂停';
  438.             });
  439.             
  440.             elements.effectsBtn.addEventListener('click', () => {
  441.                 elements.effectsPanel.classList.add('active');
  442.             });
  443.             
  444.             elements.closePanel.addEventListener('click', () => {
  445.                 elements.effectsPanel.classList.remove('active');
  446.             });
  447.             
  448.             elements.applyEffects.addEventListener('click', () => {
  449.                 elements.effectsPanel.classList.remove('active');
  450.             });
  451.             
  452.             // 特效控制
  453.             elements.effectBlur.addEventListener('input', () => {
  454.                 const value = elements.effectBlur.value;
  455.                 elements.effectBlurValue.textContent = value;
  456.                 config.effects.blur = value;
  457.             });
  458.             
  459.             elements.effectNoise.addEventListener('input', () => {
  460.                 const value = elements.effectNoise.value;
  461.                 elements.effectNoiseValue.textContent = `${value}%`;
  462.                 config.effects.noise = value;
  463.             });
  464.             
  465.             elements.effectScanlines.addEventListener('input', () => {
  466.                 const value = elements.effectScanlines.value;
  467.                 elements.effectScanlinesValue.textContent = `${value}%`;
  468.                 config.effects.scanlines = value;
  469.             });
  470.             
  471.             elements.effectGlitch.addEventListener('input', () => {
  472.                 const value = elements.effectGlitch.value;
  473.                 elements.effectGlitchValue.textContent = `${value}%`;
  474.                 config.effects.glitch = value;
  475.             });
  476.             
  477.             elements.effectPixelate.addEventListener('input', () => {
  478.                 const value = elements.effectPixelate.value;
  479.                 elements.effectPixelateValue.textContent = `${value}%`;
  480.                 config.effects.pixelate = value;
  481.             });
  482.             
  483.             // 全屏按钮
  484.             elements.fullscreenBtn.addEventListener('click', toggleFullscreen);
  485.             
  486.             // 保存按钮
  487.             elements.saveBtn.addEventListener('click', saveAsciiArt);
  488.         }
  489.         
  490.         // 初始化摄像头
  491.         function initCamera() {
  492.             navigator.mediaDevices.getUserMedia({ video: true })
  493.                 .then(stream => {
  494.                     video = document.createElement('video');
  495.                     video.srcObject = stream;
  496.                     video.autoplay = true;
  497.                     
  498.                     // 创建主画布
  499.                     canvas = document.createElement('canvas');
  500.                     ctx = canvas.getContext('2d');
  501.                     
  502.                     // 创建临时画布用于特效处理
  503.                     tempCanvas = document.createElement('canvas');
  504.                     tempCtx = tempCanvas.getContext('2d');
  505.                     
  506.                     video.onplaying = startRendering;
  507.                 })
  508.                 .catch(err => {
  509.                     elements.asciiCam.textContent = `错误: ${err.message}\n请确保已授予摄像头权限`;
  510.                     console.error('摄像头错误:', err);
  511.                 });
  512.         }
  513.         
  514.         // 开始渲染
  515.         function startRendering() {
  516.             updateResolution();
  517.             animate();
  518.         }
  519.         
  520.         // 动画循环
  521.         function animate() {
  522.             const now = performance.now();
  523.             config.frameCount++;
  524.             
  525.             // 更新FPS计数
  526.             if (now - config.lastTime >= 1000) {
  527.                 config.fps = config.frameCount;
  528.                 elements.stats.textContent = `FPS: ${config.fps} | 分辨率: ${config.width}x${config.height}`;
  529.                 config.frameCount = 0;
  530.                 config.lastTime = now;
  531.             }
  532.             
  533.             // 彩虹效果偏移
  534.             if (config.color === 'rainbow') {
  535.                 config.rainbowOffset = (config.rainbowOffset + 1) % 360;
  536.             }
  537.             
  538.             if (!config.paused) {
  539.                 renderFrame();
  540.             }
  541.             
  542.             animationId = requestAnimationFrame(animate);
  543.         }
  544.         
  545.         // 渲染帧
  546.         function renderFrame() {
  547.             // 设置画布尺寸
  548.             canvas.width = config.width;
  549.             canvas.height = config.height;
  550.             tempCanvas.width = canvas.width;
  551.             tempCanvas.height = canvas.height;
  552.             
  553.             // 绘制原始视频帧
  554.             if (config.mirror) {
  555.                 ctx.save();
  556.                 ctx.scale(-1, 1);
  557.                 ctx.drawImage(video, -canvas.width, 0, canvas.width, canvas.height);
  558.                 ctx.restore();
  559.             } else {
  560.                 ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  561.             }
  562.             
  563.             // 应用图像处理效果
  564.             applyImageEffects();
  565.             
  566.             // 获取像素数据
  567.             const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  568.             let ascii = '';
  569.             
  570.             // 转换为ASCII
  571.             for (let y = 0; y < canvas.height; y++) {
  572.                 for (let x = 0; x < canvas.width; x++) {
  573.                     const i = (y * canvas.width + x) * 4;
  574.                     const r = imgData[i];
  575.                     const g = imgData[i + 1];
  576.                     const b = imgData[i + 2];
  577.                     
  578.                     // 计算亮度 (使用感知亮度公式)
  579.                     let brightness = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
  580.                     
  581.                     // 应用对比度
  582.                     brightness = ((brightness - 0.5) * (config.contrast / 100)) + 0.5;
  583.                     
  584.                     // 应用亮度
  585.                     brightness = brightness * (config.brightness / 100);
  586.                     
  587.                     // 限制在0-1范围内
  588.                     brightness = Math.max(0, Math.min(1, brightness));
  589.                     
  590.                     // 根据亮度选择字符
  591.                     let charIndex = Math.floor(brightness * (config.chars.length - 1));
  592.                     if (config.invert) {
  593.                         charIndex = config.chars.length - 1 - charIndex;
  594.                     }
  595.                     
  596.                     // 确保索引在有效范围内
  597.                     charIndex = Math.max(0, Math.min(config.chars.length - 1, charIndex));
  598.                     
  599.                     ascii += config.chars[charIndex];
  600.                 }
  601.                 ascii += '\n';
  602.             }
  603.             
  604.             // 更新显示
  605.             elements.asciiCam.textContent = ascii;
  606.         }
  607.         
  608.         // 应用图像处理效果
  609.         function applyImageEffects() {
  610.             // 复制原始图像到临时画布
  611.             tempCtx.drawImage(canvas, 0, 0);
  612.             
  613.             // 应用模糊效果
  614.             if (config.effects.blur > 0) {
  615.                 ctx.filter = `blur(${config.effects.blur}px)`;
  616.                 ctx.drawImage(tempCanvas, 0, 0);
  617.                 ctx.filter = 'none';
  618.             }
  619.             
  620.             // 应用噪点效果
  621.             if (config.effects.noise > 0) {
  622.                 const noiseData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  623.                 const data = noiseData.data;
  624.                 const intensity = config.effects.noise / 100;
  625.                
  626.                 for (let i = 0; i < data.length; i += 4) {
  627.                     const noise = (Math.random() - 0.5) * 255 * intensity;
  628.                     data[i] += noise;     // R
  629.                     data[i + 1] += noise; // G
  630.                     data[i + 2] += noise; // B
  631.                 }
  632.                
  633.                 ctx.putImageData(noiseData, 0, 0);
  634.             }
  635.             
  636.             // 应用扫描线效果
  637.             if (config.effects.scanlines > 0) {
  638.                 const intensity = config.effects.scanlines / 100;
  639.                
  640.                 for (let y = 0; y < canvas.height; y += 2) {
  641.                     ctx.fillStyle = `rgba(0, 0, 0, ${intensity})`;
  642.                     ctx.fillRect(0, y, canvas.width, 1);
  643.                 }
  644.             }
  645.             
  646.             // 应用故障效果
  647.             if (config.effects.glitch > 0 && Math.random() < config.effects.glitch / 100) {
  648.                 const glitchAmount = Math.floor(Math.random() * 10 * (config.effects.glitch / 100));
  649.                 const glitchData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  650.                 const tempData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  651.                
  652.                 // 水平偏移
  653.                 for (let y = 0; y < canvas.height; y++) {
  654.                     const offset = Math.floor(Math.random() * glitchAmount * 2) - glitchAmount;
  655.                     if (offset !== 0) {
  656.                         for (let x = 0; x < canvas.width; x++) {
  657.                             const srcX = Math.max(0, Math.min(canvas.width - 1, x + offset));
  658.                             const srcPos = (y * canvas.width + srcX) * 4;
  659.                             const dstPos = (y * canvas.width + x) * 4;
  660.                            
  661.                             tempData.data[dstPos] = glitchData.data[srcPos];
  662.                             tempData.data[dstPos + 1] = glitchData.data[srcPos + 1];
  663.                             tempData.data[dstPos + 2] = glitchData.data[srcPos + 2];
  664.                         }
  665.                     }
  666.                 }
  667.                
  668.                 ctx.putImageData(tempData, 0, 0);
  669.             }
  670.             
  671.             // 应用像素化效果
  672.             if (config.effects.pixelate > 0) {
  673.                 const size = Math.max(1, Math.floor(config.effects.pixelate / 10));
  674.                
  675.                 if (size > 1) {
  676.                     const smallWidth = Math.floor(canvas.width / size);
  677.                     const smallHeight = Math.floor(canvas.height / size);
  678.                     
  679.                     tempCtx.drawImage(canvas, 0, 0, smallWidth, smallHeight);
  680.                     ctx.imageSmoothingEnabled = false;
  681.                     ctx.drawImage(tempCanvas, 0, 0, smallWidth, smallHeight, 0, 0, canvas.width, canvas.height);
  682.                     ctx.imageSmoothingEnabled = true;
  683.                 }
  684.             }
  685.         }
  686.         
  687.         // 更新分辨率
  688.         function updateResolution() {
  689.             const value = elements.resolution.value;
  690.             elements.resolutionValue.textContent = value;
  691.             config.width = value * 1.5;
  692.             config.height = value;
  693.         }
  694.         
  695.         // 更新颜色方案
  696.         function updateColorScheme() {
  697.             if (config.color === 'rainbow') {
  698.                 // 彩虹色不需要更新样式,因为它在动画循环中处理
  699.                 return;
  700.             }
  701.             
  702.             document.documentElement.style.setProperty('--primary-color', config.color);
  703.             elements.asciiCam.style.color = config.color;
  704.         }
  705.         
  706.         // 切换全屏
  707.         function toggleFullscreen() {
  708.             if (!document.fullscreenElement) {
  709.                 elements.asciiContainer.requestFullscreen().catch(err => {
  710.                     console.error('全屏错误:', err);
  711.                 });
  712.             } else {
  713.                 document.exitFullscreen();
  714.             }
  715.         }
  716.         
  717.         // 保存ASCII艺术
  718.         function saveAsciiArt() {
  719.             const blob = new Blob([elements.asciiCam.textContent], { type: 'text/plain' });
  720.             const url = URL.createObjectURL(blob);
  721.             const a = document.createElement('a');
  722.             a.href = url;
  723.             a.download = `ascii-art-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.txt`;
  724.             document.body.appendChild(a);
  725.             a.click();
  726.             document.body.removeChild(a);
  727.             URL.revokeObjectURL(url);
  728.         }
  729.         
  730.         // 启动应用
  731.         init();
  732.         
  733.         // 清理资源
  734.         window.addEventListener('beforeunload', () => {
  735.             if (animationId) cancelAnimationFrame(animationId);
  736.             if (video && video.srcObject) {
  737.                 video.srcObject.getTracks().forEach(track => track.stop());
  738.             }
  739.         });
  740.     </script>
  741. </body>
  742. </html>
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表