马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
用HTML5实现及时ASCII艺术摄像头
项目简介
这是一个将摄像头画面及时转换为ASCII字符艺术的Web应用,基于HTML5和原生JavaScript实现。通过本项目可以学习到:
- 欣赏器摄像头API的利用
- Canvas图像处理技能
- 及时视频流处理
- 复杂DOM操作
- 性能优化技巧
功能亮点
✨ 七大特色功能:
- 多模式字符渲染:支持8种字符集,从经典ASCII到Emoji
- 动态调色板:6种预设颜色方案+彩虹渐变结果
- 专业级图像处理:亮度/对比度调节、含糊、噪点等特效
- 分辨率控制:20-120级精致调节
- 及时特效面板:5种视觉特效独立控制
- 性能监控
:及时表现FPS和分辨率
- 数据恒久化:支持艺术作品的保存与分享
实现原理
1. 技能架构
2. 核心算法
像素到ASCII的转换公式:
- 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]
复制代码 彩虹色天生算法:
- 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. 视频流处理
- // 获取摄像头访问权限
- 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. 及时渲染引擎
- 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. 特效系统计划
- 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;
- }
- }
复制代码 参数调节发起:
场景推荐设置人脸识别高分辨率 + 标准字符集艺术创作低分辨率 + 月亮字符集 + 高噪点动态捕获中分辨率 + 二进制字符 + 高对比度性能优化
- 双缓冲技能:利用临时Canvas避免直接修改源数据
- 节流渲染:根据FPS主动调整革新频率
- Web Worker:将图像处理逻辑移至后台线程
- 内存复用:重复利用ImageData对象
- 惰性盘算:仅在参数变革时重新天生字符映射表
扩展方向
🚀 二次开发发起:
- 添加视频滤镜系统
- 集成语音控制功能
- 实现WebSocket多人共享视图
- 开发Chrome扩展版本
- 添加AR标志识别功能
完备代码实现:
- <!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>
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|