[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>GIF转TFT_eSPI</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
border: 1px solid #ccc;
padding: 20px;
border-radius: 5px;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="number"] {
width: 80px;
}
button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#preview {
margin-top: 20px;
}
#downloadBtn {
display: none;
}
.progress-container {
margin: 20px 0;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
width: 0;
height: 20px;
background-color: #4CAF50;
transition: width 0.3s ease;
}
.preview-container {
margin-top: 20px;
text-align: center;
}
.preview-container canvas {
max-width: 100%;
border: 1px solid #ccc;
margin-top: 10px;
}
.frames-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
margin-top: 20px;
}
.frame-item {
position: relative;
border: 1px solid #ccc;
padding: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.frame-item canvas {
width: 100%;
height: auto;
display: block;
}
.frame-item .progress-status {
position: absolute;
top: -8px;
right: -8px;
background: #4CAF50;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
display: none;
}
.frame-item.processing {
border-color: #FFA500;
box-shadow: 0 0 0 2px #FFA500;
}
.frame-item.completed {
border-color: #4CAF50;
box-shadow: 0 0 0 2px #4CAF50;
}
.frame-item .download-hint {
display: none;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
font-size: 12px;
padding: 2px;
text-align: center;
}
.frame-item:hover .download-hint {
display: block;
}
.preview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 20px;
}
.preview-item {
text-align: center;
}
.preview-item h3 {
margin: 5px 0;
font-size: 14px;
}
.preview-item canvas {
max-width: 100%;
height: auto;
border: 1px solid #ccc;
}
.preview-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 1000;
max-width: 90%;
max-height: 90vh;
overflow: auto;
border-radius: 8px;
}
.preview-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
input[type="range"] {
width: 200px;
margin: 0 10px;
vertical-align: middle;
}
#scalePercent {
display: inline-block;
min-width: 50px;
}
input[type="number"]:disabled {
background-color: #f5f5f5;
color: #666;
}
input[type="color"] {
width: 50px;
height: 30px;
padding: 0;
border: none;
border-radius: 4px;
cursor: pointer;
vertical-align: middle;
}
#alphaSlider {
width: 150px;
margin: 0 10px;
vertical-align: middle;
}
#alphaValue {
display: inline-block;
min-width: 80px;
vertical-align: middle;
}
#colorPreview {
display: inline-block;
width: 40px;
height: 40px;
border: 1px solid #ccc;
vertical-align: middle;
margin-left: 10px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAYdEVYdFRpdGxlAENhbnZhcyBJbWFnZSBQcmV2aWV3e0fDaAAAABV0RVh0QXV0aG9yAERhbmllbCBCZXJuZXKEXtk4AAAAGHRFWHRDcmVhdGlvbiBUaW1lADIwMTAtMDEtMDgVeXRLAAAAIXRFWHRTb3VyY2UAaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvxnML+gAAABh0RVh0U291cmNlX1VSTABodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9pbmtzY2FwZS5vcmeb7jwaAAAAKklEQVQ4jWNgGAWjYBSMAkbG/////2fAh0FyjIyMjP+JwaNgFIyCUUAJAADR4AYJh7zW1AAAAABJRU5ErkJggg==');
}
</style>
<script src="https://cdn.jsdelivr.net/npm/omggif@1.0.10/omggif.min.js"></script>
</head>
<body>
<div class="container">
<h1>GIF转TFT_eSPI</h1>
<div class="input-group">
<label for="gifFile">选择GIF文件:</label>
<input type="file" id="gifFile" accept=".gif">
</div>
<div class="input-group">
<label>尺寸设置:</label>
<input type="number" id="width" min="1" disabled> x
<input type="number" id="height" min="1" disabled>
<input type="range" id="sizeSlider" min="10" max="500" value="100">
<span id="scalePercent">100%</span>
</div>
<div class="input-group">
<label>背景颜色:</label>
<input type="color" id="colorPicker" value="#000000">
<input type="range" id="alphaSlider" min="0" max="100" value="100">
<span id="alphaValue">透明度: 100%</span>
<div id="colorPreview"></div>
</div>
<button id="convertBtn">转换</button>
<button id="downloadBtn">下载header文件</button>
<div class="progress-container">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="preview-container">
<div id="preview"></div>
<canvas id="previewCanvas"></canvas>
<div id="framesContainer" class="frames-container"></div>
</div>
</div>
<script>
let headerContent = '';
let frameCanvases = [];
let frameContents = [];
let originalWidth = 0;
let originalHeight = 0;
// RGB565 颜色转换函数
function RGB565(r, g, b) {
r = Math.round((r / 255) * 31);
g = Math.round((g / 255) * 63);
b = Math.round((b / 255) * 31);
return ((r << 11) | (g << 5) | b).toString(16).padStart(4, '0').toUpperCase();
}
// 解析GIF文件的所有帧
async function parseGif(file) {
const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);
const reader = new GifReader(buffer);
const frames = [];
// 创建临时canvas用于渲染帧
const tempCanvas = document.createElement('canvas');
tempCanvas.width = reader.width;
tempCanvas.height = reader.height;
const tempCtx = tempCanvas.getContext('2d');
const imageData = tempCtx.createImageData(reader.width, reader.height);
// 处理每一帧
for (let i = 0; i < reader.numFrames(); i++) {
// 渲染当前帧
reader.decodeAndBlitFrameRGBA(i, imageData.data);
tempCtx.putImageData(imageData, 0, 0);
// 创建新canvas存储当前帧
const canvas = document.createElement('canvas');
canvas.width = reader.width;
canvas.height = reader.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(tempCanvas, 0, 0);
frames.push({
dims: {
width: reader.width,
height: reader.height,
left: 0,
top: 0
},
canvas: canvas
});
}
return frames;
}
// 更新尺寸的函数
function updateSize(percent) {
const width = Math.round(originalWidth * percent / 100);
const height = Math.round(originalHeight * percent / 100);
document.getElementById('width').value = width;
document.getElementById('height').value = height;
document.getElementById('scalePercent').textContent = `${percent}%`;
}
// 添加滑块事件监听
document.getElementById('sizeSlider').addEventListener('input', function (e) {
updateSize(e.target.value);
});
// 文件选择事件处理
document.getElementById('gifFile').addEventListener('change', async function (e) {
const file = e.target.files[0];
if (!file) return;
document.getElementById('preview').textContent = '正在解析GIF文件...';
const framesContainer = document.getElementById('framesContainer');
framesContainer.innerHTML = '';
frameCanvases = [];
try {
const frames = await parseGif(file);
// 设置原始尺寸
originalWidth = frames[0].dims.width;
originalHeight = frames[0].dims.height;
// 重置滑块和尺寸
document.getElementById('sizeSlider').value = 100;
updateSize(100);
// 为每一帧创建预览
frames.forEach((frame, index) => {
const frameDiv = document.createElement('div');
frameDiv.className = 'frame-item';
const canvas = document.createElement('canvas');
canvas.width = frame.dims.width;
canvas.height = frame.dims.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(frame.canvas, 0, 0);
const status = document.createElement('div');
status.className = 'progress-status';
const downloadHint = document.createElement('div');
downloadHint.className = 'download-hint';
downloadHint.textContent = '点击下载此帧';
frameDiv.appendChild(canvas);
frameDiv.appendChild(status);
frameDiv.appendChild(downloadHint);
framesContainer.appendChild(frameDiv);
frameCanvases.push({ canvas, status, frameDiv });
});
document.getElementById('preview').textContent =
`GIF解析完成,共 ${frames.length} 帧,原始尺寸 ${originalWidth}x${originalHeight}。使用滑块调整尺寸,点击转换按钮开始处理。`;
} catch (error) {
document.getElementById('preview').textContent = '解析GIF文件时出错:' + error.message;
}
});
async function convertGif() {
const file = document.getElementById('gifFile').files[0];
if (!file) {
alert('请选择GIF文件');
return;
}
const width = parseInt(document.getElementById('width').value);
const height = parseInt(document.getElementById('height').value);
// 从颜色选择器获取RGB值
const color = document.getElementById('colorPicker').value;
const r = parseInt(color.substr(1, 2), 16);
const g = parseInt(color.substr(3, 2), 16);
const b = parseInt(color.substr(5, 2), 16);
// 从透明度滑块获取alpha值
const alphaPercent = parseInt(document.getElementById('alphaSlider').value);
const a = Math.round((100 - alphaPercent) * 255 / 100);
// 显示加载提示
document.getElementById('preview').textContent = '正在处理GIF文件,请稍候...';
document.getElementById('convertBtn').disabled = true;
document.getElementById('progressBar').style.width = '0%';
try {
const frames = await parseGif(file);
const frameCount = frames.length;
frameContents = [];
const previewCanvas = document.getElementById('previewCanvas');
previewCanvas.width = width;
previewCanvas.height = height;
const previewCtx = previewCanvas.getContext('2d');
// 创建离屏canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
// 生成header文件内容
headerContent = `const unsigned short PROGMEM myGifFrames[${frameCount}][${width * height}] = {\n`;
// 处理单个帧的函数
async function processFrame(frameIndex) {
const frame = frames[frameIndex];
// 清除布并填充背景色
ctx.fillStyle = `rgba(${r},${g},${b},${a / 255})`;
ctx.fillRect(0, 0, width, height);
// 绘制到主画布并调整大小
ctx.drawImage(frame.canvas, 0, 0, width, height);
// 更新预览
previewCtx.clearRect(0, 0, width, height);
previewCtx.drawImage(canvas, 0, 0);
// 获取像素数据
const imageData = ctx.getImageData(0, 0, width, height);
const pixels = imageData.data;
headerContent += '{\n';
let frameContent = `const unsigned short PROGMEM myGifFrame${frameIndex}[${width * height}] = {\n`;
// 每20行让出一次主线程
const rowsPerBatch = 20;
for (let y = 0; y < height; y++) {
if (y % rowsPerBatch === 0 && y !== 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const alpha = a === 0 ? pixels[i + 3] : a;
const blendedR = (pixels[i] * alpha + r * (255 - alpha)) / 255;
const blendedG = (pixels[i + 1] * alpha + g * (255 - alpha)) / 255;
const blendedB = (pixels[i + 2] * alpha + b * (255 - alpha)) / 255;
const rgb565 = RGB565(blendedR, blendedG, blendedB);
headerContent += `0x${rgb565}, `;
frameContent += `0x${rgb565}, `;
}
headerContent += '\n';
frameContent += '\n';
}
headerContent += '},\n';
frameContent += '};\n';
frameContents[frameIndex] = frameContent;
// 更新进度
const progress = ((frameIndex + 1) / frameCount * 100).toFixed(1);
document.getElementById('progressBar').style.width = `${progress}%`;
document.getElementById('preview').textContent =
`正在处理第 ${frameIndex + 1}/${frameCount} 帧 (${progress}%)`;
// 更新帧状态
frameCanvases.forEach((item, idx) => {
const status = item.status;
if (idx < frameIndex) {
updateFrameStatus(item, idx, 'completed');
} else if (idx === frameIndex) {
updateFrameStatus(item, idx, 'processing');
} else {
updateFrameStatus(item, idx, 'pending');
}
});
}
// 逐帧处理
for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) {
await processFrame(frameIndex);
}
headerContent += '};\n';
// 转换完成后更新所有帧的状态
frameCanvases.forEach((item, idx) => {
updateFrameStatus(item, idx, 'completed');
});
// 显示下载按钮
document.getElementById('downloadBtn').style.display = 'inline-block';
document.getElementById('preview').textContent = `转换完成!共处理 ${frameCount} 帧`;
} catch (error) {
document.getElementById('preview').textContent = '处理GIF文件时出错:' + error.message;
document.getElementById('progressBar').style.width = '0%';
frameCanvases.forEach(item => {
item.frameDiv.className = 'frame-item';
item.status.style.display = 'none';
});
} finally {
document.getElementById('convertBtn').disabled = false;
}
}
function downloadHeader() {
const blob = new Blob([headerContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'output.h';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// RGB565 转回 RGB 函数
function RGB565ToRGB(rgb565) {
const r = ((rgb565 >> 11) & 0x1F) * 255 / 31;
const g = ((rgb565 >> 5) & 0x3F) * 255 / 63;
const b = (rgb565 & 0x1F) * 255 / 31;
return [Math.round(r), Math.round(g), Math.round(b)];
}
// 创建验证预览
function createVerificationPreview(frameIndex, width, height, rgb565Data) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(width, height);
// 解析RGB565数据
const matches = rgb565Data.match(/0x[0-9A-F]{4}/g);
if (!matches) return null;
// 转换回RGB并填充imageData
for (let i = 0; i < matches.length; i++) {
const rgb565 = parseInt(matches[i], 16);
const [r, g, b] = RGB565ToRGB(rgb565);
const idx = i * 4;
imageData.data[idx] = r;
imageData.data[idx + 1] = g;
imageData.data[idx + 2] = b;
imageData.data[idx + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
// 下载单个帧的函数
function downloadSingleFrame(frameIndex) {
if (!frameContents[frameIndex]) return;
// 直接下载文件,不显示预览
const blob = new Blob([frameContents[frameIndex]], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `frame_${frameIndex}.h`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 显示比对预览
function showComparison(frameIndex) {
if (!frameContents[frameIndex]) return;
// 创建背景遮罩
const overlay = document.createElement('div');
overlay.className = 'preview-dialog-overlay';
document.body.appendChild(overlay);
// 创建预览对话框
const dialog = document.createElement('div');
dialog.className = 'preview-dialog';
const closeBtn = document.createElement('button');
closeBtn.textContent = '关闭';
closeBtn.style.position = 'absolute';
closeBtn.style.right = '10px';
closeBtn.style.top = '10px';
closeBtn.onclick = () => {
document.body.removeChild(dialog);
document.body.removeChild(overlay);
};
overlay.onclick = closeBtn.onclick;
const previewGrid = document.createElement('div');
previewGrid.className = 'preview-grid';
// 始预览
const originalPreview = document.createElement('div');
originalPreview.className = 'preview-item';
originalPreview.innerHTML = '<h3>原始图像</h3>';
const originalCanvas = document.createElement('canvas');
originalCanvas.width = frameCanvases[frameIndex].canvas.width;
originalCanvas.height = frameCanvases[frameIndex].canvas.height;
const originalCtx = originalCanvas.getContext('2d');
originalCtx.drawImage(frameCanvases[frameIndex].canvas, 0, 0);
originalPreview.appendChild(originalCanvas);
// RGB565预览
const rgb565Preview = document.createElement('div');
rgb565Preview.className = 'preview-item';
rgb565Preview.innerHTML = '<h3>转换结果</h3>';
const width = parseInt(document.getElementById('width').value);
const height = parseInt(document.getElementById('height').value);
const verificationCanvas = createVerificationPreview(frameIndex, width, height, frameContents[frameIndex]);
if (verificationCanvas) {
rgb565Preview.appendChild(verificationCanvas);
}
previewGrid.appendChild(originalPreview);
previewGrid.appendChild(rgb565Preview);
dialog.appendChild(closeBtn);
dialog.appendChild(previewGrid);
document.body.appendChild(dialog);
}
// 更新帧状态的辅助函数
function updateFrameStatus(item, idx, status) {
switch (status) {
case 'completed':
item.frameDiv.className = 'frame-item completed';
item.status.style.display = 'block';
item.status.textContent = `#${idx + 1}`;
addFrameButtons(item, idx);
break;
case 'processing':
item.frameDiv.className = 'frame-item processing';
item.status.style.display = 'block';
item.status.textContent = `处理中 #${idx + 1}`;
break;
default:
item.frameDiv.className = 'frame-item';
item.status.style.display = 'none';
}
}
// 添加帧按钮的辅助函数
function addFrameButtons(item, idx) {
const downloadHint = item.frameDiv.querySelector('.download-hint');
if (downloadHint) downloadHint.style.display = 'none';
if (!item.frameDiv.querySelector('.frame-buttons')) {
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'frame-buttons';
buttonsDiv.style.position = 'absolute';
buttonsDiv.style.bottom = '0';
buttonsDiv.style.left = '0';
buttonsDiv.style.right = '0';
buttonsDiv.style.display = 'none';
buttonsDiv.style.justifyContent = 'space-around';
buttonsDiv.style.background = 'rgba(0, 0, 0, 0.7)';
buttonsDiv.style.padding = '2px';
const previewBtn = document.createElement('button');
previewBtn.textContent = '比对';
previewBtn.style.padding = '2px 5px';
previewBtn.style.fontSize = '12px';
previewBtn.onclick = (e) => {
e.stopPropagation();
showComparison(idx);
};
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '下载';
downloadBtn.style.padding = '2px 5px';
downloadBtn.style.fontSize = '12px';
downloadBtn.onclick = (e) => {
e.stopPropagation();
downloadSingleFrame(idx);
};
buttonsDiv.appendChild(previewBtn);
buttonsDiv.appendChild(downloadBtn);
item.frameDiv.appendChild(buttonsDiv);
// 鼠标悬停时显示按钮
item.frameDiv.onmouseenter = () => buttonsDiv.style.display = 'flex';
item.frameDiv.onmouseleave = () => buttonsDiv.style.display = 'none';
}
}
// 添加按钮事件监听器
document.getElementById('convertBtn').addEventListener('click', convertGif);
document.getElementById('downloadBtn').onclick = downloadHeader;
// 更新颜色预览的函数
function updateColorPreview() {
const color = document.getElementById('colorPicker').value;
const alpha = (100 - parseInt(document.getElementById('alphaSlider').value)) / 100;
document.getElementById('colorPreview').style.backgroundColor = color;
document.getElementById('colorPreview').style.opacity = alpha;
}
// 颜色选择器和透明度滑块事件监听
document.getElementById('colorPicker').addEventListener('input', updateColorPreview);
document.getElementById('alphaSlider').addEventListener('input', function (e) {
document.getElementById('alphaValue').textContent = `透明度: ${e.target.value}%`;
updateColorPreview();
});
// 初始化颜色预览
updateColorPreview();
</script>
</body>
</html>