下拉刷新
OneFile
源码
ascii-cam把视频图像转成 ascii
作者 iveseenthedark·主语言 HTML·无依赖·3.4k 次查看
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="author" content="iveseenthedark" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#333333" /> <link rel="apple-touch-icon" sizes="180x180" href="https://img.hellogithub.com/favicon/apple-touch-icon.png"> <link rel="android-chrome" sizes="192x192" href="https://img.hellogithub.com/favicon/android-chrome-192x192.png"> <link rel="android-chrome" sizes="512x512" href="https://img.hellogithub.com/favicon/android-chrome-512x512.png"> <link rel="icon" type="image/png" sizes="32x32" href="https://img.hellogithub.com/favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="https://img.hellogithub.com/favicon/favicon-16x16.png"> <link rel="icon" href="https://img.hellogithub.com/favicon/favicon.ico"> <title>A S C I I C A M</title> <style> #ascii-cam pre, body, html { width: 100vw; height: 100vh; margin: 0; } * { box-sizing: border-box; } body { overflow: hidden; background-color: #000; color: #eee; font-family: monospace; } #logo, #enable-cam-msg { transform: translate(-50%, -50%); position: absolute; left: 50%; top: 50%; } .fade { transition: opacity 0.25s ease-in-out; } #logo { line-height: 1.5; } #ascii-cam { opacity: 0; } #ascii-cam pre { position: absolute; top: 0; left: 0; overflow: hidden; line-height: 0.6; user-select: none; mix-blend-mode: screen; font-size: 1.2em; font-size: 2vh; } @media (max-height: 50em) { #ascii-cam pre { font-size: 1em; } } @media (min-height: 60em) { #ascii-cam pre { font-size: 1.2em; } } #ascii-cam #r-output { color: red; } #ascii-cam #g-output { color: #0f0; } #ascii-cam #b-output { color: #00f; } #enable-cam-msg { opacity: 0; font-size: 3rem; background-color: white; color: black; margin: 0; max-width: 50vw; white-space: pre-line; text-align: center; font-weight: 900; } #canvas, #video { display: none; } </style> </head> <body> <pre id="logo" class="fade"> _n_|_|_,_ |===.-.===| | ((_)) | '==='-'===' A S C I I C A M 点击“允许”使用摄像头 </pre> <pre id="enable-cam-msg" class="fade">Allow access to camera to use</pre> <div id="ascii-cam" class="fade"> <div class="layers"> <pre id="r-output"></pre> <pre id="g-output"></pre> <pre id="b-output"></pre> </div> </div> <video id="video" autoplay="true"></video> <canvas id="canvas" width="640" height="480"></canvas> <script type="application/javascript"> (() => { 'use strict'; let interval; let r_layer, g_layer, b_layer; const palette = [' ', '.', '-', ':', '+', '*', '=', '%', '@', '#']; const vanity = 'B Y I V E S E E N T H E D A R K'; /** * @param {*} stream */ const handle_video = stream => { const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const video = document.getElementById('video'); // Older browsers may not have srcObject if ('srcObject' in video) { video.srcObject = stream; } else { // Avoid using this in new browsers, as it is going away. video.src = window.URL.createObjectURL(stream); } video.addEventListener('loadedmetadata', () => { window.addEventListener('resize', () => set_canvas_dimensions(video, canvas, ctx)); set_canvas_dimensions(video, canvas, ctx); // Hide Logo play_video(video, canvas, ctx); setTimeout(() => { document.getElementById('logo').style.opacity = 0; document.getElementById('ascii-cam').style.opacity = 1; }, 1000); }); }; /** */ const handle_no_video = () => { setTimeout(() => { document.getElementById('logo').style.opacity = 0; document.getElementById('ascii-cam').style.opacity = 1; document.getElementById('enable-cam-msg').style.opacity = 1; play_random(); }, 1000); }; /** * @param {*} e */ const handle_error = e => { console.error('Error', e); handle_no_video(); }; /** * The greyscale value is calculated GREY = 0.299 * RED + 0.587 * GREEN + 0.114 * BLUE * For more information see http://en.wikipedia.org/wiki/Grayscale * @param {Array} image_data */ const fade_to_grey = pixels => pixels.map(pixel => pixel[0] * 0.299 + pixel[1] * 0.587 + pixel[2] * 0.114); /** */ const play_video = (video, canvas, ctx) => { clearInterval(interval); // Buffer reverse ctx.drawImage(video, 0, 0, canvas.width * -1, canvas.height); // Grab view const dx = canvas.dataset.hoffset || 0; const dy = canvas.dataset.voffset || 0; const image_width = canvas.width - dx * 2; const image_data = ctx.getImageData(dx, dy, image_width, canvas.height - dy * 2).data; // Chunk const channels = 4; const pixels = Array.from(Array(Math.ceil(image_data.length / channels)), (_, i) => image_data.slice(i * channels, i * channels + channels) ); // Draw let r_output = ''; let g_output = ''; let b_output = ''; pixels.forEach((pixel, i) => { // Break for new line if (i && i % image_width === 0) { r_output += '\n'; g_output += '\n'; b_output += '\n'; } const b_idx = Math.floor((pixel[2] / 255.0) * (palette.length - 1)); const r_idx = Math.floor((pixel[0] / 255.0) * (palette.length - 1)); const g_idx = Math.floor((pixel[1] / 255.0) * (palette.length - 1)); if (i && i >= image_width - vanity.length && i < image_width) { // Input vanity message r_output += vanity[vanity.length + i - image_width]; g_output += vanity[vanity.length + i - image_width]; b_output += palette[g_idx]; } else { // Output camera data r_output += palette[r_idx]; g_output += palette[g_idx]; b_output += palette[b_idx]; } }, this); r_layer.textContent = r_output; g_layer.textContent = g_output; b_layer.textContent = b_output; interval = setInterval(() => play_video(video, canvas, ctx), 33); }; /** */ const play_random = () => { clearInterval(interval); const char_dims = get_character_dimensions(); const screen_dims = get_screen_dimensions(char_dims); const total_chars = Math.floor(screen_dims.char_width * screen_dims.char_height); const mid_x = Math.floor(screen_dims.char_width / 2); const mid_y = Math.floor(screen_dims.char_height / 2); let radius_sq = mid_x * mid_x + mid_y + mid_y; radius_sq = radius_sq < 2500 ? 2500 : radius_sq; // Stop the circle getting too small let r_output = '', g_output = '', b_output = ''; for (let i = 0; i < total_chars; i++) { // Break for new line if (i && i % screen_dims.char_width === 0) { r_output += '\n'; g_output += '\n'; b_output += '\n'; } const char_pos_x = i % screen_dims.char_width; const char_pos_y = i / screen_dims.char_width; let dist = (char_pos_x - mid_x) * (char_pos_x - mid_x) + (char_pos_y - mid_y) * (char_pos_y - mid_y); dist = dist > radius_sq ? radius_sq : dist; // Distance can be further than radius, clamp const range = Math.floor(palette.length * (1 - dist / radius_sq)); const r_idx = Math.floor(Math.random() * range); const g_idx = Math.floor(Math.random() * range); const b_idx = Math.floor(Math.random() * range); r_output += palette[r_idx]; g_output += palette[g_idx]; b_output += palette[b_idx]; } r_layer.textContent = r_output; g_layer.textContent = g_output; b_layer.textContent = b_output; interval = setInterval(play_random, 100); }; /** */ const get_character_dimensions = () => { const span = document.createElement('span'); span.textContent = 'X'; span.style.position = 'absolute'; span.style.left = '-100px'; document.querySelector('#ascii-cam pre').appendChild(span); const dimensions = span.getBoundingClientRect(); span.remove(); return dimensions; }; /** */ const get_screen_dimensions = char_dims => { return { width: window.innerWidth, height: window.innerHeight, char_width: Math.ceil(window.innerWidth / char_dims.width), char_height: Math.ceil(window.innerHeight / char_dims.height) }; }; /** */ const set_canvas_dimensions = (video, canvas, ctx) => { console.log('setting canvas dimensions'); const char_dims = get_character_dimensions(); const screen_dims = get_screen_dimensions(char_dims); let width = Math.floor(screen_dims.width / char_dims.width); let height = Math.max(width * 0.75, screen_dims.char_height); if (height > screen_dims.char_height) { canvas.dataset.voffset = Math.floor((height - screen_dims.char_height) / 2); canvas.dataset.hoffset = 0; } else { width = (height / 3) * 4; canvas.dataset.hoffset = Math.floor((width - screen_dims.char_width) / 2); canvas.dataset.voffset = 0; } video.width = canvas.width = width; video.height = canvas.height = height; // Reset scale if context exists ctx && ctx.scale(-1, 1); }; // Onload window.addEventListener('load', () => { if (navigator && navigator.mediaDevices) { // Grab layers r_layer = document.getElementById('r-output'); g_layer = document.getElementById('g-output'); b_layer = document.getElementById('b-output'); const constraints = { audio: false, video: {} }; navigator.mediaDevices .getUserMedia(constraints) .then(handle_video) .catch(handle_error); } else { handle_no_video(); } }); })(); </script> </body> </html>