You've already forked firmware
mirror of
https://github.com/FlipperMCE/firmware.git
synced 2026-02-16 16:55:36 -08:00
670 lines
25 KiB
HTML
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> |