Some more tools around Android icons
All checks were successful
/ publish (push) Successful in 1m48s
All checks were successful
/ publish (push) Successful in 1m48s
This commit is contained in:
parent
7f6dcac154
commit
c7ff8597aa
15 changed files with 644 additions and 2 deletions
34
site/android-icons/index.html
Normal file
34
site/android-icons/index.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Android Icon Resizer - Tools</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="container">
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1>Android Icon Resizer</h1>
|
||||
<p>Resize PNG images to Android mipmap density buckets. Everything runs in your browser.</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
<main>
|
||||
<div>
|
||||
<label for="file-input">Select PNG files</label>
|
||||
<input type="file" id="file-input" accept="image/png" multiple>
|
||||
</div>
|
||||
<div id="preview-area"></div>
|
||||
<div id="circle-preview">
|
||||
<h3>Circle Crop Preview</h3>
|
||||
<canvas id="circle-canvas" width="192" height="192"></canvas>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="prepare-btn" disabled>Prepare</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</main>
|
||||
<script src="main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
136
site/android-icons/main.js
Normal file
136
site/android-icons/main.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import "/wasm/wasm_exec.js";
|
||||
|
||||
const go = new Go();
|
||||
let wasmReady = false;
|
||||
|
||||
WebAssembly.instantiateStreaming(fetch("/wasm/android-icons.wasm"), go.importObject)
|
||||
.then((result) => {
|
||||
go.run(result.instance);
|
||||
wasmReady = true;
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const previewArea = document.getElementById("preview-area");
|
||||
const prepareBtn = document.getElementById("prepare-btn");
|
||||
const statusEl = document.getElementById("status");
|
||||
const circlePreview = document.getElementById("circle-preview");
|
||||
const circleCanvas = document.getElementById("circle-canvas");
|
||||
|
||||
let loadedFiles = [];
|
||||
|
||||
function renderCirclePreview() {
|
||||
if (loadedFiles.length === 0) {
|
||||
circlePreview.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const nameLower = (f) => f.name.toLowerCase();
|
||||
|
||||
let bgFile = loadedFiles.find((f) => /back/.test(nameLower(f)));
|
||||
let fgFile = loadedFiles.find((f) => !/back/.test(nameLower(f)) && !/mono/.test(nameLower(f)));
|
||||
|
||||
// Fallbacks: if only one image or no "back" image, use the first file
|
||||
if (!bgFile && !fgFile) {
|
||||
bgFile = loadedFiles[0];
|
||||
} else if (!bgFile) {
|
||||
bgFile = fgFile;
|
||||
fgFile = null;
|
||||
}
|
||||
|
||||
const size = 192;
|
||||
circleCanvas.width = size;
|
||||
circleCanvas.height = size;
|
||||
const ctx = circleCanvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
// Clip to circle
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
|
||||
const drawLayer = (src) => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, size, size);
|
||||
resolve();
|
||||
};
|
||||
img.onerror = resolve;
|
||||
img.src = src;
|
||||
});
|
||||
};
|
||||
|
||||
drawLayer(bgFile.data).then(() => {
|
||||
if (fgFile && fgFile !== bgFile) {
|
||||
return drawLayer(fgFile.data);
|
||||
}
|
||||
}).then(() => {
|
||||
ctx.restore();
|
||||
|
||||
// Draw border ring
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size / 2 - 1.5, 0, Math.PI * 2);
|
||||
ctx.closePath();
|
||||
ctx.strokeStyle = "rgba(128, 128, 128, 0.5)";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
|
||||
circlePreview.style.display = "block";
|
||||
});
|
||||
}
|
||||
|
||||
fileInput.addEventListener("change", () => {
|
||||
const files = Array.from(fileInput.files);
|
||||
loadedFiles = [];
|
||||
previewArea.innerHTML = "";
|
||||
circlePreview.style.display = "none";
|
||||
|
||||
if (files.length === 0) {
|
||||
prepareBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
files.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const dataURL = e.target.result;
|
||||
loadedFiles.push({ name: file.name, data: dataURL });
|
||||
|
||||
const item = document.createElement("div");
|
||||
item.className = "preview-item";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = dataURL;
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.textContent = file.name;
|
||||
|
||||
item.appendChild(img);
|
||||
item.appendChild(label);
|
||||
previewArea.appendChild(item);
|
||||
|
||||
if (loadedFiles.length === files.length) {
|
||||
prepareBtn.disabled = false;
|
||||
renderCirclePreview();
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
});
|
||||
|
||||
prepareBtn.addEventListener("click", () => {
|
||||
if (!wasmReady) {
|
||||
statusEl.textContent = "WASM module is still loading, please wait...";
|
||||
return;
|
||||
}
|
||||
if (loadedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = "Preparing...";
|
||||
// Pass file data to Go WASM
|
||||
setTimeout(() => {
|
||||
prepareZip(loadedFiles);
|
||||
}, 50);
|
||||
});
|
||||
54
site/android-icons/style.css
Normal file
54
site/android-icons/style.css
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#preview-area {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.preview-item img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: contain;
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: 4px;
|
||||
background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 50% / 16px 16px;
|
||||
}
|
||||
|
||||
.preview-item span {
|
||||
font-size: 0.8rem;
|
||||
color: var(--pico-muted-color);
|
||||
word-break: break-all;
|
||||
max-width: 96px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
#circle-preview {
|
||||
display: none;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
#circle-preview h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#circle-canvas {
|
||||
background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 50% / 16px 16px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 3px var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 0.5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue