Files
firmware/misc/splashgen.html
2025-09-03 11:50:11 +02:00

670 lines
25 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlipperMCE Splash Generator</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h2>FlipperMCE Splash Generator</h2>
<input type="file" id="imgInput" accept="image/*" aria-label="Upload image" style="display:none;">
<div class="controls">
<div style="display:flex; flex-direction:column; gap:8px; width:100%; max-width:700px;">
<div style="display:flex; align-items:center; gap:18px; flex-wrap:wrap;">
<span style="font-weight:600; color:#6cb6ff; min-width:90px;">Threshold</span>
<input type="range" id="threshSlider" min="0" max="255" value="128" style="width: 200px;"
title="Set the threshold for black/white conversion (0=black, 255=white)">
<span id="threshVal" style="min-width:36px; color:#2d8cf0; font-weight:600;">128</span>
</div>
<div style="font-weight:600; color:#6cb6ff; margin-top:8px;">Grayscale Weights</div>
<div style="display:flex; align-items:center; gap:18px; flex-wrap:wrap;">
<label title="Red channel weight (auto-normalized)">R
<input type="range" id="rSlider" min="0" max="1" step="0.01" value="0.299"
style="width: 100px; margin-left:6px;">
<span id="rVal" style="min-width:36px; color:#ff6b6b; font-weight:600;">0.299</span>
</label>
<label title="Green channel weight (auto-normalized)">G
<input type="range" id="gSlider" min="0" max="1" step="0.01" value="0.587"
style="width: 100px; margin-left:6px;">
<span id="gVal" style="min-width:36px; color:#51fa7b; font-weight:600;">0.587</span>
</label>
<label title="Blue channel weight (auto-normalized)">B
<input type="range" id="bSlider" min="0" max="1" step="0.01" value="0.114"
style="width: 100px; margin-left:6px;">
<span id="bVal" style="min-width:36px; color:#6cb6ff; font-weight:600;">0.114</span>
</label>
<span style="font-size: 0.9em; color: #aaa; margin-left:10px;">(weights auto-normalized)</span>
</div>
<div style="display:flex; justify-content:flex-end; margin-top:4px; gap:10px;">
<button id="resetBtn"
style="padding:5px 18px; border-radius:7px; background:#232a36; color:#e3e6eb; border:1.5px solid #3a4252; font-weight:600; cursor:pointer;">Reset</button>
<button id="clearSelBtn"
style="padding:5px 18px; border-radius:7px; background:#2d2a36; color:#ff6b6b; border:1.5px solid #6b2d2d; font-weight:600; cursor:pointer;">Clear
Selection</button>
</div>
</div>
</div>
<div class="container">
<div class="img-block">
<span>Original</span>
<div id="origImgWrap" role="button" tabindex="0" aria-label="Drop image here or click to upload"
style="position:relative; display:inline-block; width:305px; height:305px; min-width:305px; min-height:305px; max-width:305px; max-height:305px; border:2.5px dashed #bfc9d1; border-radius:10px; background:#f8fafc; transition:border-color 0.2s, background 0.2s; cursor:pointer;">
<img id="origImg" src="" alt=""
style="display:block; width:100%; height:100%; object-fit:contain; position:relative; z-index:1;" />
<canvas id="selectCanvas"
style="position:absolute; left:0; top:0; width:100%; height:100%; pointer-events:auto; z-index:2; background:transparent;"></canvas>
<div id="dropOverlay"
style="display:none; position:absolute; left:0; top:0; width:100%; height:100%; background:rgba(108,182,255,0.10); border-radius:10px; z-index:10; display:flex; align-items:center; justify-content:center; color:#6cb6ff; font-size:1.2em; font-weight:600; pointer-events:none; text-align:center; transition:background 0.2s;">
Drop image here</div>
</div>
</div>
<div class="img-block">
<span>Black & White</span>
<canvas id="bwCanvas"></canvas>
<div style="display:flex; flex-direction:row; gap:10px; margin-top:14px;">
<button id="downloadBinBtn"
style="font-size:1em; padding:7px 18px; border-radius:7px; background:#6cb6ff; color:#181c24; border:none; cursor:pointer; box-shadow:0 2px 8px 0 rgba(108,182,255,0.18);">Download
Binary</button>
<button id="downloadUf2Btn"
style="font-size:1em; padding:7px 18px; border-radius:7px; background:#6cb6ff; color:#181c24; border:none; cursor:pointer; box-shadow:0 2px 8px 0 rgba(108,182,255,0.18);">Download
UF2</button>
<button id="downloadCombinedBtn"
style="font-size:1em; padding:7px 18px; border-radius:7px; background:#6cb6ff; color:#181c24; border:none; cursor:pointer; box-shadow:0 2px 8px 0 rgba(108,182,255,0.18);">Download
Combined UF2</button>
</div>
<div id="firmwareDropzone" style="margin-top:18px; padding:24px 18px; border:2px dashed #6cb6ff; border-radius:10px; background:#232a36; color:#6cb6ff; text-align:center; font-weight:600; cursor:pointer; transition:border-color 0.2s, background 0.2s;">Drop firmware UF2 here or click to select</div>
</div>
</div>
</div>
<div style="max-width:900px; margin: 28px auto 0 auto; width: 98vw;">
<label for="cArrayBox" style="font-weight:600; color:#6cb6ff; margin-bottom:6px; display:block;">C Header
Array:</label>
<textarea id="cArrayBox" readonly aria-label="C header array"
style="width:100%; min-width:220px; max-width:100%; height:160px; font-family:monospace; font-size:0.98em; background:#232a36; color:#e3e6eb; border-radius:7px; border:1.5px solid #3a4252; padding:8px; resize:vertical; display:block;"></textarea>
</div>
<script src="uf2.js"></script>
<script defer>
// --- DOM Queries ---
const origImgWrap = document.getElementById('origImgWrap');
const dropOverlay = document.getElementById('dropOverlay');
const imgInput = document.getElementById('imgInput');
const origImg = document.getElementById('origImg');
const bwCanvas = document.getElementById('bwCanvas');
const bwCtx = bwCanvas.getContext('2d');
const threshSlider = document.getElementById('threshSlider');
const threshVal = document.getElementById('threshVal');
const rSlider = document.getElementById('rSlider');
const gSlider = document.getElementById('gSlider');
const bSlider = document.getElementById('bSlider');
const rVal = document.getElementById('rVal');
const gVal = document.getElementById('gVal');
const bVal = document.getElementById('bVal');
const cArrayBox = document.getElementById('cArrayBox');
// --- Drag and drop image loading for origImgWrap ---
origImgWrap.addEventListener('dragover', function (e) {
e.preventDefault();
dropOverlay.style.display = 'flex';
origImgWrap.style.borderColor = '#2d8cf0';
origImgWrap.style.background = '#e3ecfa';
});
origImgWrap.addEventListener('dragleave', function (e) {
dropOverlay.style.display = 'none';
origImgWrap.style.borderColor = '';
origImgWrap.style.background = '';
});
origImgWrap.addEventListener('drop', function (e) {
e.preventDefault();
dropOverlay.style.display = 'none';
origImgWrap.style.borderColor = '';
origImgWrap.style.background = '';
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
if (!file.type.startsWith('image/')) {
alert('Please drop an image file.');
return;
}
const reader = new FileReader();
reader.onload = function (ev) {
origImg.src = ev.target.result;
};
reader.readAsDataURL(file);
}
});
// Keyboard accessibility: allow Enter/Space to trigger file input
origImgWrap.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
imgInput.click();
}
});
// Reset controls to default values
document.getElementById('resetBtn').addEventListener('click', function () {
threshSlider.value = 128;
rSlider.value = 0.299;
gSlider.value = 0.587;
bSlider.value = 0.114;
updateLabels();
updateBW();
});
// --- Binary and C array export helpers ---
function getBWBinaryArray() {
const outW = 128, outH = 64;
const ctx = bwCanvas.getContext('2d');
const imgData = ctx.getImageData(0, 0, outW, outH);
const data = imgData.data;
const numBytes = (outW * outH) / 8;
const bin = new Uint8Array(numBytes);
let byte = 0, bit = 0, idx = 0;
for (let y = 0; y < outH; ++y) {
for (let x = 0; x < outW; ++x) {
const i = (y * outW + x) * 4;
const v = data[i];
if (v > 127) byte |= (1 << (7 - bit));
bit++;
if (bit === 8) {
bin[idx++] = byte;
byte = 0;
bit = 0;
}
}
}
return bin;
}
// Use the same color table for both binary and C array
const COLOR_TABLE = [0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
function bwBinaryToCArray(bin, arrName = "bw_image") {
let out = `const uint8_t ${arrName}[${bin.length + COLOR_TABLE.length}] = {\n `;
let col = 0;
for (let i = 0; i < COLOR_TABLE.length; ++i, ++col) {
out += `0x${COLOR_TABLE[i].toString(16).padStart(2, '0')}`;
if (i !== bin.length + COLOR_TABLE.length - 1) out += ',';
if ((col + 1) % 8 === 0 && i !== bin.length + COLOR_TABLE.length - 1) { out += '\n '; col = -1; }
else if (i !== bin.length + COLOR_TABLE.length - 1) out += ' ';
}
for (let i = 0; i < bin.length; ++i, ++col) {
out += `0x${bin[i].toString(16).padStart(2, '0')}`;
if (i !== bin.length - 1) out += ',';
if ((col + 1) % 8 === 0 && i !== bin.length - 1) { out += '\n '; col = -1; }
else if (i !== bin.length - 1) out += ' ';
}
out += '\n};';
return out;
}
function updateCArrayBox() {
const bin = getBWBinaryArray();
document.getElementById('cArrayBox').value = bwBinaryToCArray(bin);
}
function downloadBWBinary() {
// Use the same color table as C array
const colorTable = new Uint8Array(COLOR_TABLE);
const bin = getBWBinaryArray();
const full = new Uint8Array(colorTable.length + bin.length);
full.set(colorTable, 0);
full.set(bin, colorTable.length);
const blob = new Blob([full], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'bw_image.bin';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
}
// Attach download handler
document.getElementById('downloadBinBtn').addEventListener('click', downloadBWBinary);
// Auto-select C array textarea on click
document.getElementById('cArrayBox').addEventListener('click', function (e) {
this.select();
});
const selectCanvas = document.getElementById('selectCanvas');
const selectCtx = selectCanvas.getContext('2d');
let isSelecting = false;
let selectStart = null;
let selection = null; // {x, y, w, h}
let isDraggingSelection = false;
let dragOffset = { x: 0, y: 0 };
let imgLoaded = false;
// --- Main BW update logic ---
function updateBW() {
if (!imgLoaded) return;
// Use the selected region if present, else full image
const origW = origImg.naturalWidth;
const origH = origImg.naturalHeight;
let crop = { x: 0, y: 0, w: origW, h: origH };
if (selection && origImg.width && origImg.height) {
// Map selection from display to natural image coordinates
const scaleX = origW / origImg.width;
const scaleY = origH / origImg.height;
crop = {
x: Math.max(0, Math.round(selection.x * scaleX)),
y: Math.max(0, Math.round(selection.y * scaleY)),
w: Math.max(1, Math.round(selection.w * scaleX)),
h: Math.max(1, Math.round(selection.h * scaleY))
};
// Clamp
if (crop.x + crop.w > origW) crop.w = origW - crop.x;
if (crop.y + crop.h > origH) crop.h = origH - crop.y;
// If selection is too small, fallback to full image
if (crop.w < 2 || crop.h < 2) {
crop = { x: 0, y: 0, w: origW, h: origH };
}
}
// Determine target size based on crop
let targetW, targetH;
if (crop.w / crop.h >= 128 / 64) {
targetW = 128;
targetH = Math.round(crop.h * (128 / crop.w));
if (targetH > 64) {
targetH = 64;
targetW = Math.round(crop.w * (64 / crop.h));
}
} else {
targetH = 64;
targetW = Math.round(crop.w * (64 / crop.h));
if (targetW > 128) {
targetW = 128;
targetH = Math.round(crop.h * (128 / crop.w));
}
}
// Always output 128x64
const outW = 128, outH = 64;
// Calculate scaled image size
let drawW = targetW, drawH = targetH;
// Center the image
let offsetX = Math.floor((outW - drawW) / 2);
let offsetY = Math.floor((outH - drawH) / 2);
// Create a temp canvas for the scaled image
const tempCanvas = document.createElement('canvas');
tempCanvas.width = drawW;
tempCanvas.height = drawH;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.clearRect(0, 0, drawW, drawH);
tempCtx.drawImage(origImg, crop.x, crop.y, crop.w, crop.h, 0, 0, drawW, drawH);
// Get image data from temp
const imgData = tempCtx.getImageData(0, 0, drawW, drawH);
const data = imgData.data;
// Normalize weights
let rW = parseFloat(rSlider.value);
let gW = parseFloat(gSlider.value);
let bW = parseFloat(bSlider.value);
const sum = rW + gW + bW;
rW /= sum; gW /= sum; bW /= sum;
// Convert to grayscale and apply threshold
const threshold = parseInt(threshSlider.value);
for (let i = 0; i < data.length; i += 4) {
const avg = rW * data[i] + gW * data[i + 1] + bW * data[i + 2];
const bw = avg >= threshold ? 255 : 0;
data[i] = data[i + 1] = data[i + 2] = bw;
}
tempCtx.putImageData(imgData, 0, 0);
// Prepare output canvas
bwCanvas.width = outW;
bwCanvas.height = outH;
// Use putImageData for performance
// Create a blank image buffer
const bwImgData = bwCtx.createImageData(outW, outH);
const bwData = bwImgData.data;
// Fill with black
for (let i = 0; i < bwData.length; i += 4) {
bwData[i] = 0;
bwData[i + 1] = 0;
bwData[i + 2] = 0;
bwData[i + 3] = 255;
}
// Copy processed image into center
const tempBW = tempCtx.getImageData(0, 0, drawW, drawH).data;
for (let y = 0; y < drawH; ++y) {
for (let x = 0; x < drawW; ++x) {
const srcIdx = (y * drawW + x) * 4;
const dstIdx = ((y + offsetY) * outW + (x + offsetX)) * 4;
bwData[dstIdx] = tempBW[srcIdx];
bwData[dstIdx + 1] = tempBW[srcIdx + 1];
bwData[dstIdx + 2] = tempBW[srcIdx + 2];
bwData[dstIdx + 3] = 255;
}
}
bwCtx.putImageData(bwImgData, 0, 0);
// Only update C array box once, after all drawing is done
updateCArrayBox();
// Upscale the canvas to match the original image's display size
const displayW = origImg.clientWidth;
const displayH = origImg.clientHeight;
//if (displayW && displayH) {
// bwCanvas.style.width = '128px';
// bwCanvas.style.height = '64px';
//} else {
bwCanvas.style.width = '300px';
bwCanvas.style.height = 'auto';
//}
}
imgInput.addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function (ev) {
origImg.src = ev.target.result;
};
reader.readAsDataURL(file);
});
// Error handling for image load
origImg.onerror = function () {
imgLoaded = false;
alert('Failed to load image. Please try another file.');
};
origImg.onload = function () {
imgLoaded = true;
// Sync overlay size to image
function syncOverlay() {
const w = origImg.offsetWidth;
const h = origImg.offsetHeight;
if (w && h) {
selectCanvas.width = w;
selectCanvas.height = h;
selectCanvas.style.width = w + 'px';
selectCanvas.style.height = h + 'px';
selectCanvas.style.display = '';
drawSelection();
updateBW();
} else {
requestAnimationFrame(syncOverlay);
}
}
syncOverlay();
};
// Clear selection button logic
document.getElementById('clearSelBtn').addEventListener('click', function () {
selection = null;
drawSelection();
updateBW();
});
function drawSelection() {
selectCtx.clearRect(0, 0, selectCanvas.width, selectCanvas.height);
if (selection) {
selectCtx.save();
// Only draw border
selectCtx.strokeStyle = '#2d8cf0';
selectCtx.lineWidth = 2;
selectCtx.setLineDash([6, 4]);
selectCtx.strokeRect(selection.x + 0.5, selection.y + 0.5, selection.w, selection.h);
selectCtx.restore();
}
}
function updateLabels() {
threshVal.textContent = threshSlider.value;
rVal.textContent = parseFloat(rSlider.value).toFixed(3);
gVal.textContent = parseFloat(gSlider.value).toFixed(3);
bVal.textContent = parseFloat(bSlider.value).toFixed(3);
}
// --- Selection overlay logic ---
selectCanvas.addEventListener('mousedown', function (e) {
if (!imgLoaded) return;
const rect = selectCanvas.getBoundingClientRect();
const mouseX = Math.max(0, Math.min(e.clientX - rect.left, selectCanvas.width));
const mouseY = Math.max(0, Math.min(e.clientY - rect.top, selectCanvas.height));
// If inside selection, start dragging
if (selection &&
mouseX >= selection.x && mouseX <= selection.x + selection.w &&
mouseY >= selection.y && mouseY <= selection.y + selection.h) {
isDraggingSelection = true;
dragOffset.x = mouseX - selection.x;
dragOffset.y = mouseY - selection.y;
} else {
isSelecting = true;
selectStart = {
x: mouseX,
y: mouseY
};
}
});
selectCanvas.addEventListener('mousemove', function (e) {
const rect = selectCanvas.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, selectCanvas.width));
const y = Math.max(0, Math.min(e.clientY - rect.top, selectCanvas.height));
if (isDraggingSelection && selection) {
// Move selection
let newX = x - dragOffset.x;
let newY = y - dragOffset.y;
// Clamp to canvas
newX = Math.max(0, Math.min(newX, selectCanvas.width - selection.w));
newY = Math.max(0, Math.min(newY, selectCanvas.height - selection.h));
selection.x = newX;
selection.y = newY;
drawSelection();
} else if (isSelecting) {
const sx = selectStart.x;
const sy = selectStart.y;
selection = {
x: Math.min(sx, x),
y: Math.min(sy, y),
w: Math.abs(x - sx),
h: Math.abs(y - sy)
};
drawSelection();
}
});
window.addEventListener('mouseup', function (e) {
if (isSelecting) {
isSelecting = false;
if (selection && (selection.w < 5 || selection.h < 5)) {
// Too small, clear
selection = null;
}
drawSelection();
updateBW();
}
if (isDraggingSelection) {
isDraggingSelection = false;
drawSelection();
updateBW();
}
});
window.addEventListener('resize', function () {
if (!imgLoaded) return;
function syncOverlayResize() {
const w = origImg.offsetWidth;
const h = origImg.offsetHeight;
if (w && h) {
selectCanvas.width = w;
selectCanvas.height = h;
selectCanvas.style.width = w + 'px';
selectCanvas.style.height = h + 'px';
drawSelection();
} else {
requestAnimationFrame(syncOverlayResize);
}
}
syncOverlayResize();
});
// (Selection overlay removed)
// Update labels live as the slider moves
threshSlider.addEventListener('input', function () {
threshVal.textContent = threshSlider.value;
updateBW();
});
rSlider.addEventListener('input', function () {
rVal.textContent = parseFloat(rSlider.value).toFixed(3);
updateBW();
});
gSlider.addEventListener('input', function () {
gVal.textContent = parseFloat(gSlider.value).toFixed(3);
updateBW();
});
bSlider.addEventListener('input', function () {
bVal.textContent = parseFloat(bSlider.value).toFixed(3);
updateBW();
});
// Only update BW image when the user releases the slider
threshSlider.addEventListener('change', function () {
updateLabels();
updateBW();
});
rSlider.addEventListener('change', function () {
updateLabels();
updateBW();
});
gSlider.addEventListener('change', function () {
updateLabels();
updateBW();
});
bSlider.addEventListener('change', function () {
updateLabels();
updateBW();
});
updateLabels();
// Download UF2 handler
let logoUf2 = null;
document.getElementById('downloadUf2Btn').addEventListener('click', function () {
const colorTable = new Uint8Array(COLOR_TABLE);
const bin = getBWBinaryArray();
const full = new Uint8Array(colorTable.length + bin.length);
full.set(colorTable, 0);
full.set(bin, colorTable.length);
logoUf2 = payloadToUf2(full.buffer);
const blob = new Blob([logoUf2], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'logo.uf2';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
// Firmware UF2 upload and combine logic
let uploadedFirmwareUf2 = null;
const firmwareDropzone = document.getElementById('firmwareDropzone');
firmwareDropzone.addEventListener('dragover', function(e) {
e.preventDefault();
firmwareDropzone.style.borderColor = '#2d8cf0';
firmwareDropzone.style.background = '#2d3a56';
});
firmwareDropzone.addEventListener('dragleave', function(e) {
firmwareDropzone.style.borderColor = '#6cb6ff';
firmwareDropzone.style.background = '#232a36';
});
firmwareDropzone.addEventListener('drop', function(e) {
e.preventDefault();
firmwareDropzone.style.borderColor = '#6cb6ff';
firmwareDropzone.style.background = '#232a36';
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
if (!file.name.endsWith('.uf2')) {
alert('Please drop a UF2 firmware file.');
return;
}
const reader = new FileReader();
reader.onload = function(ev) {
uploadedFirmwareUf2 = ev.target.result;
firmwareDropzone.textContent = 'Firmware UF2 uploaded: ' + file.name;
};
reader.readAsArrayBuffer(file);
}
});
// Optional: click to select file
firmwareDropzone.addEventListener('click', function() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.uf2';
input.style.display = 'none';
input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
uploadedFirmwareUf2 = ev.target.result;
firmwareDropzone.textContent = 'Firmware UF2 uploaded: ' + file.name;
};
reader.readAsArrayBuffer(file);
};
document.body.appendChild(input);
input.click();
setTimeout(() => document.body.removeChild(input), 1000);
});
document.getElementById('downloadCombinedBtn').addEventListener('click', function() {
if (!uploadedFirmwareUf2) {
alert('Please upload a firmware UF2 file first.');
return;
}
const colorTable = new Uint8Array(COLOR_TABLE);
const bin = getBWBinaryArray();
const full = new Uint8Array(colorTable.length + bin.length);
full.set(colorTable, 0);
full.set(bin, colorTable.length);
const logoUf2 = payloadToUf2(full.buffer);
// Merge using uf2.js
const merged = mergeUf2Files(uploadedFirmwareUf2, logoUf2.buffer);
const blob = new Blob([merged], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'combined.uf2';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
</script>
</body>
</html>