

我们可以通过算法来完成很多很多的功能,所以就有了一个想法,将各类工具都写出来,当然是尽可能的,毕竟未来无限可期,很多功能是我们当前还想不到的,为了最为靠谱的方法来完成,这里选择的语言为 HTML 来完成,别看只是简单的页面操作,但是里面都是各类算法来完成的,并且有很多的js库,做起来会很方便,能节约很多的时间,本系列文章会很多,有些标题命名可能不太合适,如果你用到了,感觉不好找可以在文章下面留言,我看到了后会进行对应的修改,希望本系列文章能给大家提供到各种各样的遍历。
图片转ASCII码工具是一个纯前端Web应用,能够将上传的图片实时转换为由ASCII字符组成的文本图像。该工具完全在浏览器中运行,无需后端支持,保护用户隐私的同时提供高效的图片转换体验。
处理大尺寸图片(≥1000px)时可能出现短暂卡顿,这是因为像素计算量大幅增加。当前的解决方案是:
本工具完全在浏览器本地运行,不会将图片或处理结果上传到任何服务器,保证用户数据的隐私安全。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片转ASCII码工具</title>
<style>
:root {
--bg-color: #f8f9fa;
--text-color: #212529;
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--border-color: #dee2e6;
--card-bg: #ffffff;
--hover-color: #e9ecef;
}
.dark-mode {
--bg-color: #212529;
--text-color: #f8f9fa;
--primary-color: #0d6efd;
--secondary-color: #adb5bd;
--border-color: #495057;
--card-bg: #343a40;
--hover-color: #495057;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
transition: background-color 0.3s, color 0.3s;
padding: 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border-color);
}
h1 {
font-size: 1.8rem;
color: var(--primary-color);
}
.theme-toggle {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-color);
transition: transform 0.2s;
}
.theme-toggle:hover {
transform: scale(1.1);
}
.container {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
max-width: 1200px;
margin: 0 auto;
}
.card {
background-color: var(--card-bg);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 20px;
transition: transform 0.3s;
}
.upload-section {
display: flex;
flex-direction: column;
align-items: center;
}
.upload-area {
width: 100%;
height: 200px;
border: 2px dashed var(--border-color);
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
transition: border-color 0.3s, background-color 0.3s;
margin-bottom: 20px;
position: relative;
}
.upload-area:hover {
border-color: var(--primary-color);
background-color: var(--hover-color);
}
.upload-area i {
font-size: 3rem;
margin-bottom: 10px;
color: var(--secondary-color);
}
.upload-area p {
color: var(--secondary-color);
text-align: center;
}
.image-preview {
max-width: 300px;
max-height: 300px;
margin: 15px 0;
border-radius: 8px;
display: none;
}
.file-input {
display: none;
}
.result-section {
display: flex;
flex-direction: column;
}
.ascii-output {
background-color: var(--card-bg);
border-radius: 8px;
padding: 15px;
overflow: auto;
height: 400px;
font-family: Consolas, monospace;
white-space: pre;
line-height: 1;
font-size: 10px;
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.inverted {
background-color: var(--text-color);
color: var(--bg-color);
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 20px;
}
.control-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.slider-container {
display: flex;
align-items: center;
}
.slider {
flex-grow: 1;
height: 5px;
background-color: var(--border-color);
outline: none;
border-radius: 5px;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background-color: var(--primary-color);
cursor: pointer;
}
.slider-value {
min-width: 40px;
text-align: right;
margin-left: 10px;
}
select {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--text-color);
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.btn {
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, background-color 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
opacity: 0.9;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
}
.btn-outline {
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-color);
}
.toggle-btn {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--text-color);
cursor: pointer;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--secondary-color);
transition: .4s;
border-radius: 20px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--primary-color);
}
input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.loading {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
color: white;
justify-content: center;
align-items: center;
border-radius: 8px;
font-size: 1.2rem;
}
.loading::after {
content: "处理中...";
animation: dots 1.5s infinite;
}
@keyframes dots {
0%, 20% { content: "处理中."; }
40% { content: "处理中.."; }
60%, 100% { content: "处理中..."; }
}
.notification {
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 20px;
background-color: var(--primary-color);
color: white;
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transform: translateY(100px);
opacity: 0;
transition: transform 0.3s, opacity 0.3s;
}
.notification.show {
transform: translateY(0);
opacity: 1;
}
footer {
margin-top: 30px;
text-align: center;
color: var(--secondary-color);
font-size: 0.9rem;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
/* 移动端适配 */
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
.controls {
grid-template-columns: 1fr;
}
.button-group {
flex-direction: column;
gap: 10px;
}
.btn {
width: 100%;
}
header {
flex-direction: column;
align-items: flex-start;
}
.theme-toggle {
position: absolute;
top: 20px;
right: 20px;
}
}
</style>
</head>
<body>
<header>
<h1>图片转ASCII码工具</h1>
<button class="theme-toggle" id="themeToggle">🌙</button>
</header>
<div class="container">
<div class="card upload-section">
<div class="upload-area" id="uploadArea">
<div class="loading" id="loading"></div>
<i>📁</i>
<p>点击或拖拽图片到此处上传<br><small>支持jpg/png/webp格式,≤5MB</small></p>
<input type="file" id="fileInput" class="file-input" accept="image/jpeg,image/png,image/webp">
</div>
<img id="imagePreview" class="image-preview" alt="预览图">
</div>
<div class="result-section">
<pre id="asciiOutput" class="ascii-output">ASCII结果将显示在这里...</pre>
<div class="controls">
<div class="control-group">
<label for="widthSlider">字符密度 (宽度)</label>
<div class="slider-container">
<input type="range" id="widthSlider" class="slider" min="50" max="200" value="100">
<span id="widthValue" class="slider-value">100</span>
</div>
</div>
<div class="control-group">
<label for="charsetSelect">字符集</label>
<select id="charsetSelect">
<option value="simple">精简版 (10字符)</option>
<option value="full">完整版 (32字符)</option>
</select>
</div>
<div class="control-group">
<label>反色效果</label>
<button id="invertBtn" class="toggle-btn">
<span>黑底白字</span>
<label class="toggle-switch">
<input type="checkbox" id="invertToggle">
<span class="toggle-slider"></span>
</label>
</button>
</div>
<div class="control-group">
<label for="fontSizeSlider">字体大小</label>
<div class="slider-container">
<input type="range" id="fontSizeSlider" class="slider" min="6" max="16" value="10">
<span id="fontSizeValue" class="slider-value">10px</span>
</div>
</div>
</div>
<div class="button-group">
<button id="copyBtn" class="btn btn-primary" disabled>复制ASCII</button>
<button id="downloadBtn" class="btn btn-secondary" disabled>下载结果</button>
<button id="resetBtn" class="btn btn-outline">重置</button>
</div>
</div>
</div>
<footer>
<p>图片转ASCII码工具 - 纯前端实现,无需联网,保护您的隐私</p>
</footer>
<div class="notification" id="notification"></div>
<script>
// DOM元素
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const imagePreview = document.getElementById('imagePreview');
const asciiOutput = document.getElementById('asciiOutput');
const widthSlider = document.getElementById('widthSlider');
const widthValue = document.getElementById('widthValue');
const charsetSelect = document.getElementById('charsetSelect');
const invertToggle = document.getElementById('invertToggle');
const fontSizeSlider = document.getElementById('fontSizeSlider');
const fontSizeValue = document.getElementById('fontSizeValue');
const copyBtn = document.getElementById('copyBtn');
const downloadBtn = document.getElementById('downloadBtn');
const resetBtn = document.getElementById('resetBtn');
const themeToggle = document.getElementById('themeToggle');
const notification = document.getElementById('notification');
const loading = document.getElementById('loading');
// 状态变量
let currentImage = null;
let fileName = '';
let isDarkMode = false;
// ASCII字符集
const charsets = {
simple: ' .,:;ox%#@', // 精简版 (10字符)
full: ' .`^",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$' // 完整版 (32字符)
};
// 初始化
function init() {
// 事件监听
uploadArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('dragleave', handleDragLeave);
uploadArea.addEventListener('drop', handleDrop);
widthSlider.addEventListener('input', updateWidthValue);
widthSlider.addEventListener('change', processImage);
charsetSelect.addEventListener('change', processImage);
invertToggle.addEventListener('change', toggleInvert);
fontSizeSlider.addEventListener('input', updateFontSize);
copyBtn.addEventListener('click', copyAscii);
downloadBtn.addEventListener('click', downloadAscii);
resetBtn.addEventListener('click', resetApp);
themeToggle.addEventListener('click', toggleTheme);
// 初始化字体大小
updateFontSize();
}
// 文件处理函数
function handleFileSelect(e) {
const file = e.target.files[0];
processFile(file);
}
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
uploadArea.style.borderColor = 'var(--primary-color)';
uploadArea.style.backgroundColor = 'var(--hover-color)';
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
uploadArea.style.borderColor = 'var(--border-color)';
uploadArea.style.backgroundColor = '';
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
uploadArea.style.borderColor = 'var(--border-color)';
uploadArea.style.backgroundColor = '';
const file = e.dataTransfer.files[0];
processFile(file);
}
// 处理上传的文件
function processFile(file) {
// 验证文件类型
if (!file || !file.type.match('image/(jpeg|png|webp)')) {
showNotification('请上传图片格式文件 (jpg/png/webp)');
return;
}
// 验证文件大小
if (file.size > 5 * 1024 * 1024) {
showNotification('文件需≤5MB');
return;
}
fileName = file.name;
// 读取文件
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
currentImage = img;
displayPreview(img);
processImage();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 显示图片预览
function displayPreview(img) {
// 计算预览图尺寸,保持比例
const maxSize = 300;
let width = img.width;
let height = img.height;
if (width > height) {
if (width > maxSize) {
height = height * (maxSize / width);
width = maxSize;
}
} else {
if (height > maxSize) {
width = width * (maxSize / height);
height = maxSize;
}
}
// 显示预览图
imagePreview.src = img.src;
imagePreview.style.display = 'block';
imagePreview.style.width = `${width}px`;
imagePreview.style.height = `${height}px`;
// 启用按钮
copyBtn.disabled = false;
downloadBtn.disabled = false;
}
// 处理图片转ASCII
function processImage() {
if (!currentImage) return;
// 显示加载动画
loading.style.display = 'flex';
// 使用setTimeout让UI有时间更新
setTimeout(() => {
const width = parseInt(widthSlider.value);
const charset = charsets[charsetSelect.value];
// 计算高度,保持比例
const ratio = currentImage.height / currentImage.width;
const height = Math.floor(width * ratio * 0.5); // 乘0.5是因为字符在终端中高宽比约为2:1
// 创建canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
// 绘制图片到canvas
ctx.drawImage(currentImage, 0, 0, width, height);
// 获取像素数据
const imageData = ctx.getImageData(0, 0, width, height);
const pixels = imageData.data;
// 转换为ASCII
let asciiImage = '';
// 对于大图片,采用隔行采样以提高性能
const skipFactor = width > 150 ? 2 : 1;
for (let y = 0; y < height; y += skipFactor) {
for (let x = 0; x < width; x += skipFactor) {
const idx = (y * width + x) * 4;
// 计算灰度值 (使用加权平均法)
const r = pixels[idx];
const g = pixels[idx + 1];
const b = pixels[idx + 2];
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
// 映射灰度值到字符
const charIndex = Math.floor(gray / 256 * charset.length);
asciiImage += charset[charIndex];
}
asciiImage += '\n';
}
// 显示ASCII结果
asciiOutput.textContent = asciiImage;
// 隐藏加载动画
loading.style.display = 'none';
}, 100);
}
// 更新宽度值显示
function updateWidthValue() {
widthValue.textContent = widthSlider.value;
}
// 更新字体大小
function updateFontSize() {
const size = fontSizeSlider.value;
fontSizeValue.textContent = `${size}px`;
asciiOutput.style.fontSize = `${size}px`;
}
// 切换反色效果
function toggleInvert() {
if (invertToggle.checked) {
asciiOutput.classList.add('inverted');
} else {
asciiOutput.classList.remove('inverted');
}
}
// 复制ASCII到剪贴板
function copyAscii() {
const text = asciiOutput.textContent;
navigator.clipboard.writeText(text)
.then(() => showNotification('已复制到剪贴板'))
.catch(err => showNotification('复制失败: ' + err));
}
// 下载ASCII结果
function downloadAscii() {
const text = asciiOutput.textContent;
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ascii_${fileName.split('.')[0]}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification('文件已下载');
}
// 重置应用
function resetApp() {
currentImage = null;
fileName = '';
imagePreview.style.display = 'none';
asciiOutput.textContent = 'ASCII结果将显示在这里...';
widthSlider.value = 100;
updateWidthValue();
charsetSelect.value = 'simple';
invertToggle.checked = false;
toggleInvert();
fontSizeSlider.value = 10;
updateFontSize();
copyBtn.disabled = true;
downloadBtn.disabled = true;
fileInput.value = '';
}
// 切换深色/浅色模式
function toggleTheme() {
isDarkMode = !isDarkMode;
if (isDarkMode) {
document.body.classList.add('dark-mode');
themeToggle.textContent = '☀️';
} else {
document.body.classList.remove('dark-mode');
themeToggle.textContent = '🌙';
}
}
// 显示通知
function showNotification(message) {
notification.textContent = message;
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// 初始化应用
init();
</script>
</body>
</html>