Diese HTML Datei ist keine eigentliche App, aber sie funktioniert genau so ;-)
Einfach die HTML Datei auf Euer Handy laden und aufrufen. 
Video dazu gibt es auf meinem Kanal ;-)

HEAD-Code
Hier nix
HTML-Code
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Verpixler – Offline Bild-Verpixelung</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--bg-page: #f2f4f8;
--bg-card: #ffffff;
--border-soft: #dde2ea;
--accent: #007bff;
--accent-dark: #0056b3;
--success: #28a745;
--success-dark: #218838;
--text-main: #222;
--text-muted: #666;
--danger: #dc3545;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background: var(--bg-page);
padding: 16px;
color: var(--text-main);
}
.container {
max-width: 960px;
margin: 0 auto;
background: var(--bg-card);
padding: 20px 18px 26px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(15,23,42,0.08);
}
header {
border-bottom: 1px solid var(--border-soft);
padding-bottom: 10px;
margin-bottom: 14px;
}
header h1 {
font-size: 1.4rem;
margin-bottom: 4px;
}
header p {
font-size: 0.9rem;
color: var(--text-muted);
}
.section {
margin-top: 14px;
margin-bottom: 14px;
padding: 10px 10px 12px;
border-radius: 10px;
border: 1px solid var(--border-soft);
background: #f9fafb;
}
.section h2 {
font-size: 1.05rem;
margin-bottom: 4px;
}
.section small {
font-size: 0.8rem;
color: var(--text-muted);
}
input[type="file"], input[type="number"], select {
padding: 8px;
margin: 8px 0;
border-radius: 6px;
border: 1px solid #d0d7e2;
font-size: 0.9rem;
}
input[type="number"] {
width: 120px;
max-width: 100%;
}
button {
background: var(--accent);
color: #fff;
padding: 9px 16px;
border: none;
border-radius: 999px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 6px;
margin-right: 8px;
transition: background 0.2s, opacity 0.2s;
}
button:hover { background: var(--accent-dark); }
button.secondary {
background: #e5e7eb;
color: #111827;
}
button.secondary:hover { background: #d4d4d8; }
button.success-flash {
background: var(--success);
}
button.success-flash:hover {
background: var(--success-dark);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.image-wrapper {
margin-top: 10px;
}
.canvas-container {
position: relative;
border-radius: 10px;
overflow: auto;
max-height: 70vh;
border: 1px solid var(--border-soft);
background: #f1f5f9;
touch-action: none;
-webkit-user-select: none;
user-select: none;
/* Smooth scrolling für besseres Pan-Gefühl */
-webkit-overflow-scrolling: touch;
}
canvas {
display: block;
width: 100%;
height: auto;
}
.selection-box {
position: absolute;
border: 2px dashed var(--accent);
background: rgba(0,123,255,0.18);
pointer-events: none;
display: none;
border-radius: 4px;
}
.fixed-region {
position: absolute;
border: 2px solid var(--success);
background: rgba(40, 167, 69, 0.2);
pointer-events: auto;
border-radius: 4px;
cursor: grab;
}
.region-control {
position: absolute;
top: -10px;
right: -10px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--danger);
color: white;
text-align: center;
line-height: 20px;
font-size: 10px;
font-weight: bold;
cursor: pointer;
pointer-events: all;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.hint {
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 6px;
}
.pill {
display: inline-block;
font-size: 0.8rem;
padding: 3px 8px;
border-radius: 999px;
background: #e5e7eb;
color: #374151;
margin-left: 4px;
}
.footer {
text-align:center;
margin-top: 20px;
font-size: 0.8rem;
color: var(--text-muted);
}
.loading-hint {
color: var(--accent);
font-weight: 500;
margin-top: 8px;
display: none;
}
/* Mode Indicator für bessere UX */
.mode-indicator {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
color: white;
padding: 6px 12px;
border-radius: 999px;
font-size: 0.8rem;
display: none;
z-index: 10;
pointer-events: none;
}
@media (max-width: 600px) {
.container {
padding: 16px 12px 20px;
}
input[type="number"] {
margin-bottom: 12px;
}
.canvas-container {
max-height: 60vh;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Verpixler – Offline Bild-Verpixelung</h1>
<p>Markiere Gesichter oder Bereiche und verpixle sie lokal ohne Server, ohne Upload.</p>
</header>
<section class="section">
<h2>1. Bild auswählen</h2>
<small><b><font color="#FF0000">Bild bleibt lokal auf deinem Gerät. Es wird nicht hochgeladen.</font></b></small><br>
<input type="file" id="fileInput" accept="image/*">
<div class="loading-hint" id="loadingHint">Bild wird geladen...</div>
</section>
<section class="section">
<h2>2. Bereiche markieren &amp; bearbeiten</h2>
<small>Bei Zoom &gt;100%: Halte 0,5s für Pan-Modus, ziehe sofort für neue Auswahl.</small>
<div style="margin-top:8px;margin-bottom:6px;">
Methode wählen:
<select id="operationMode">
<option value="pixelate">Verpixeln (Blöcke)</option>
<option value="cover">Abdecken (Voller Balken)</option>
</select>
</div>
<div style="margin-bottom:12px;">
<span id="pixelateOptions">
Pixelgröße (5–80):
<input type="number" id="pixelSize" min="5" max="80" value="25">
<span class="pill">größer = gröber</span>
</span>
<span id="coverOptions" style="display:none;">
Balkenfarbe:
<input type="color" id="coverColor" value="#000000">
</span>
</div>
<div style="margin-bottom:12px;">
<label for="zoomSlider" style="font-size:0.9rem;font-weight:600;display:block;margin-bottom:4px;">
Zoom: <span id="zoomLabel">100%</span>
</label>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<button type="button" id="zoomOutBtn" class="secondary" style="padding:6px 12px;">➖</button>
<input type="range" id="zoomSlider" min="0.5" max="3" step="0.01" value="1"
style="flex:1;min-width:180px;">
<button type="button" id="zoomInBtn" class="secondary" style="padding:6px 12px;">➕</button>
</div>
<div class="hint">Bei Zoom kannst du das Bild verschieben (0,5s halten) oder neue Bereiche markieren (sofort ziehen).</div>
</div>
<div style="margin-bottom: 8px;">
<button id="applyBtn" disabled>🎨 Bearbeitung auf markierte Bereiche anwenden</button>
<button id="clearRegionsBtn" class="secondary" disabled>✖ Alle Auswahl löschen</button>
</div>
<div class="hint">
Ausgewählte Bereiche: <strong id="regionsCount">0</strong>
</div>
<div class="image-wrapper">
<div class="canvas-container" id="canvasContainer" style="display:none;">
<div class="mode-indicator" id="modeIndicator">📍 Pan-Modus</div>
<canvas id="canvas"></canvas>
<div class="selection-box" id="selectionBox"></div>
</div>
<div class="hint" id="noImageHint" style="margin-top:8px;">
Noch kein Bild geladen. Wähle oben ein Bild aus.
</div>
</div>
</section>
<section class="section">
<h2>3. Ergebnis speichern &amp; zurücksetzen</h2>
<small>Speichert das aktuell angezeigte Bild (inkl. Bearbeitung) in deine Galerie/Downloads.</small><br>
<div style="margin-top: 8px;">
<button id="saveBtn" disabled>💾 Bearbeitetes Bild speichern</button>
<button id="resetImageBtn" class="secondary" disabled>↩ Originalbild wiederherstellen</button>
</div>
</section>
<div class="footer">
&copy; Volker Niederastroth – Verpixler (lokal, ohne Server)
</div>
</div>
<script>
(function(){
const fileInput = document.getElementById('fileInput');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const container = document.getElementById('canvasContainer');
const selectionBox = document.getElementById('selectionBox');
const regionsCountEl = document.getElementById('regionsCount');
const clearBtn = document.getElementById('clearRegionsBtn');
const applyBtn = document.getElementById('applyBtn');
const saveBtn = document.getElementById('saveBtn');
const resetImageBtn = document.getElementById('resetImageBtn');
const noImageHint = document.getElementById('noImageHint');
const loadingHint = document.getElementById('loadingHint');
const modeIndicator = document.getElementById('modeIndicator');
const operationMode = document.getElementById('operationMode');
const pixelSizeInput = document.getElementById('pixelSize');
const pixelateOptions = document.getElementById('pixelateOptions');
const coverOptions = document.getElementById('coverOptions');
const coverColorInput = document.getElementById('coverColor');
const zoomSlider = document.getElementById('zoomSlider');
const zoomLabel = document.getElementById('zoomLabel');
const zoomInBtn = document.getElementById('zoomInBtn');
const zoomOutBtn = document.getElementById('zoomOutBtn');
let zoomScale = 1;
function applyZoom() {
if (!imgLoaded) return;
canvas.style.width = (zoomScale * 100) + '%';
canvas.style.height = 'auto';
if (zoomLabel) zoomLabel.textContent = Math.round(zoomScale * 100) + '%';
redrawRegions();
}
function setZoom(newScale) {
zoomScale = Math.max(0.5, Math.min(3, newScale));
if (zoomSlider) zoomSlider.value = zoomScale;
applyZoom();
}
let img = new Image();
let imgLoaded = false;
let regions = [];
let originalFileName = 'vepixler_bild';
let isDrawing = false;
let startX = 0, startY = 0;
function resetAll() {
regions = [];
updateRegionsCount();
selectionBox.style.display = 'none';
if (imgLoaded) {
drawImage();
}
redrawRegions();
}
function updateRegionsCount() {
regionsCountEl.textContent = regions.length;
clearBtn.disabled = regions.length === 0;
applyBtn.disabled = !imgLoaded || regions.length === 0;
}
function drawImage() {
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
saveBtn.disabled = false;
}
function redrawRegions() {
const existing = container.querySelectorAll('.fixed-region');
existing.forEach(el => el.remove());
if (!imgLoaded || regions.length === 0) return;
const rect = canvas.getBoundingClientRect();
const scaleX = rect.width / canvas.width;
const scaleY = rect.height / canvas.height;
regions.forEach((r, index) => {
const dispX = r.x * scaleX;
const dispY = r.y * scaleY;
const dispW = r.width * scaleX;
const dispH = r.height * scaleY;
const div = document.createElement('div');
div.className = 'fixed-region';
div.dataset.index = index;
div.style.left = dispX + 'px';
div.style.top = dispY + 'px';
div.style.width = dispW + 'px';
div.style.height = dispH + 'px';
const deleteBtn = document.createElement('div');
deleteBtn.className = 'region-control';
deleteBtn.textContent = 'X';
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteRegion(index);
};
div.appendChild(deleteBtn);
container.appendChild(div);
});
}
function deleteRegion(index) {
regions.splice(index, 1);
updateRegionsCount();
redrawRegions();
}
function getPos(evt) {
const rect = canvas.getBoundingClientRect();
const clientX = evt.touches ? evt.touches[0].clientX : evt.clientX;
const clientY = evt.touches ? evt.touches[0].clientY : evt.clientY;
let x = clientX - rect.left;
let y = clientY - rect.top;
x = Math.max(0, Math.min(x, rect.width));
y = Math.max(0, Math.min(y, rect.height));
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
cx: x, cy: y,
x: x * scaleX,
y: y * scaleY
};
}
function startDraw(evt) {
if (!imgLoaded) return;
evt.preventDefault();
const p = getPos(evt);
isDrawing = true;
startX = p.cx;
startY = p.cy;
selectionBox.style.display = 'block';
selectionBox.style.left = startX + 'px';
selectionBox.style.top = startY + 'px';
selectionBox.style.width = '0px';
selectionBox.style.height = '0px';
}
function moveDraw(evt) {
if (!isDrawing) return;
evt.preventDefault();
const p = getPos(evt);
const currentX = p.cx;
const currentY = p.cy;
const left = Math.min(startX, currentX);
const top = Math.min(startY, currentY);
const width = Math.abs(currentX - startX);
const height = Math.abs(currentY - startY);
selectionBox.style.left = left + 'px';
selectionBox.style.top = top + 'px';
selectionBox.style.width = width + 'px';
selectionBox.style.height = height + 'px';
}
function endDraw(evt) {
if (!isDrawing) return;
evt.preventDefault();
isDrawing = false;
const style = window.getComputedStyle(selectionBox);
const dispW = parseFloat(style.width);
const dispH = parseFloat(style.height);
if (dispW < 5 || dispH < 5) {
selectionBox.style.display = 'none';
return;
}
const dispX = parseFloat(style.left);
const dispY = parseFloat(style.top);
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const imgX = Math.round(dispX * scaleX);
const imgY = Math.round(dispY * scaleY);
const imgW = Math.round(dispW * scaleX);
const imgH = Math.round(dispH * scaleY);
regions.push({
x: imgX,
y: imgY,
width: imgW,
height: imgH
});
updateRegionsCount();
redrawRegions();
selectionBox.style.display = 'none';
}
function applyRegionsOperation() {
if (!imgLoaded || regions.length === 0) return;
drawImage();
const mode = operationMode.value;
let successText = '';
regions.forEach(region => {
const rx = region.x;
const ry = region.y;
const rw = region.width;
const rh = region.height;
if (mode === 'pixelate') {
pixelateRegion(rx, ry, rw, rh);
successText = '✅ Verpixelt!';
} else if (mode === 'cover') {
coverRegion(rx, ry, rw, rh);
successText = '⬛ Abgedeckt!';
}
});
regions = [];
updateRegionsCount();
redrawRegions();
saveBtn.disabled = false;
flashSuccess(applyBtn, successText);
}
function pixelateRegion(rx, ry, rw, rh) {
let pixelSize = parseInt(pixelSizeInput.value) || 25;
pixelSize = Math.max(5, Math.min(80, pixelSize));
pixelSizeInput.value = pixelSize;
for (let y = ry; y < ry + rh; y += pixelSize) {
for (let x = rx; x < rx + rw; x += pixelSize) {
const bw = Math.min(pixelSize, rx + rw - x);
const bh = Math.min(pixelSize, ry + rh - y);
const sampleX = Math.min(canvas.width - 1, x + Math.floor(bw / 2));
const sampleY = Math.min(canvas.height - 1, y + Math.floor(bh / 2));
const pixel = ctx.getImageData(sampleX, sampleY, 1, 1).data;
const r = pixel[0];
const g = pixel[1];
const b = pixel[2];
const a = pixel[3] / 255;
ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
ctx.fillRect(x, y, bw, bh);
}
}
}
function coverRegion(rx, ry, rw, rh) {
const color = coverColorInput.value || '#000000';
ctx.fillStyle = color;
ctx.fillRect(rx, ry, rw, rh);
}
function updateOptionsDisplay() {
if (operationMode.value === 'pixelate') {
pixelateOptions.style.display = 'inline';
coverOptions.style.display = 'none';
} else if (operationMode.value === 'cover') {
pixelateOptions.style.display = 'none';
coverOptions.style.display = 'inline';
}
}
function flashSuccess(button, text) {
const oldHtml = button.innerHTML;
button.classList.add('success-flash');
button.innerHTML = text;
setTimeout(() => {
button.classList.remove('success-flash');
button.innerHTML = oldHtml;
}, 1500);
}
function saveImage() {
if (!imgLoaded) return;
const link = document.createElement('a');
const baseName = originalFileName.replace(/\.[^/.]+$/, "");
link.download = baseName + '_bearbeitet_' + Date.now() + '.png';
link.href = canvas.toDataURL('image/png');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function showModeIndicator(text) {
modeIndicator.textContent = text;
modeIndicator.style.display = 'block';
setTimeout(() => {
modeIndicator.style.display = 'none';
}, 1500);
}
// --- Verbesserte Touch-Steuerung mit längerem Timer für Pan ---
let autoAction = null;
let pendingTimer = null;
let downClientX = 0, downClientY = 0;
let downCX = 0, downCY = 0;
let panStartScrollLeft = 0, panStartScrollTop = 0;
let draggingRegionIndex = -1;
let dragRegionOffsetX = 0, dragRegionOffsetY = 0;
function clearPendingTimer(){
if (pendingTimer) {
clearTimeout(pendingTimer);
pendingTimer = null;
}
}
function startRegionDrag(evt, regionIndex){
const pos = getPos(evt);
draggingRegionIndex = regionIndex;
autoAction = 'dragRegion';
const r = regions[regionIndex];
dragRegionOffsetX = pos.x - r.x;
dragRegionOffsetY = pos.y - r.y;
clearPendingTimer();
}
function moveRegionDrag(evt){
if (autoAction !== 'dragRegion' || draggingRegionIndex < 0) return;
evt.preventDefault();
const pos = getPos(evt);
const r = regions[draggingRegionIndex];
r.x = pos.x - dragRegionOffsetX;
r.y = pos.y - dragRegionOffsetY;
r.x = Math.max(0, Math.min(r.x, canvas.width - r.width));
r.y = Math.max(0, Math.min(r.y, canvas.height - r.height));
redrawRegions();
}
function endRegionDrag(){
draggingRegionIndex = -1;
autoAction = null;
}
function onAutoDown(evt){
if (!imgLoaded) return;
if (evt.button !== undefined && evt.button !== 0) return;
if (evt.target && evt.target.closest && evt.target.closest('.region-control')) {
return;
}
const fixed = evt.target && evt.target.closest ? evt.target.closest('.fixed-region') : null;
if (fixed) {
const idx = parseInt(fixed.dataset.index, 10);
if (!isNaN(idx)) {
evt.preventDefault();
startRegionDrag(evt, idx);
return;
}
}
evt.preventDefault();
const pEvt = evt.touches ? evt.touches[0] : evt;
downClientX = pEvt.clientX;
downClientY = pEvt.clientY;
const p = getPos(evt);
downCX = p.cx;
downCY = p.cy;
panStartScrollLeft = container.scrollLeft;
panStartScrollTop = container.scrollTop;
autoAction = 'pending';
clearPendingTimer();
// VERLÄNGERTER Timer: 500ms statt 200ms für besseres Pan-Gefühl
pendingTimer = setTimeout(() => {
if (autoAction === 'pending') {
autoAction = 'pan';
// Optional: Zeige Indikator an
if (zoomScale > 1.1) {
showModeIndicator('📍 Pan-Modus aktiv');
}
}
}, 500);
}
function onAutoMove(evt){
if (!imgLoaded || !autoAction) return;
const pEvt = evt.touches ? evt.touches[0] : evt;
const dxClient = pEvt.clientX - downClientX;
const dyClient = pEvt.clientY - downClientY;
const dist = Math.hypot(dxClient, dyClient);
if (autoAction === 'dragRegion') {
moveRegionDrag(evt);
return;
}
if (autoAction === 'pending') {
// Niedrigerer Schwellenwert: 10px statt 6px
if (dist > 10) {
clearPendingTimer();
autoAction = 'draw';
isDrawing = true;
startX = downCX;
startY = downCY;
selectionBox.style.display = 'block';
selectionBox.style.left = startX + 'px';
selectionBox.style.top = startY + 'px';
selectionBox.style.width = '0px';
selectionBox.style.height = '0px';
if (zoomScale > 1.1) {
showModeIndicator('✏️ Auswahl-Modus');
}
} else {
return;
}
}
if (autoAction === 'draw') {
moveDraw(evt);
return;
}
if (autoAction === 'pan') {
evt.preventDefault();
container.scrollLeft = panStartScrollLeft - dxClient;
container.scrollTop = panStartScrollTop - dyClient;
return;
}
}
function onAutoUp(evt){
if (!autoAction) return;
clearPendingTimer();
if (autoAction === 'dragRegion') {
endRegionDrag();
return;
}
if (autoAction === 'draw') {
endDraw(evt);
autoAction = null;
return;
}
autoAction = null;
}
container.addEventListener('mousedown', onAutoDown);
container.addEventListener('mousemove', onAutoMove);
container.addEventListener('mouseup', onAutoUp);
container.addEventListener('mouseleave', onAutoUp);
container.addEventListener('touchstart', onAutoDown, { passive: false });
container.addEventListener('touchmove', onAutoMove, { passive: false });
container.addEventListener('touchend', onAutoUp, { passive: false });
container.addEventListener('touchcancel',onAutoUp, { passive: false });
fileInput.addEventListener('change', function(e){
const file = e.target.files[0];
if (!file) return;
originalFileName = file.name;
loadingHint.style.display = 'block';
noImageHint.style.display = 'none';
const reader = new FileReader();
reader.onload = function(ev){
img = new Image();
img.onload = function(){
imgLoaded = true;
container.style.display = 'block';
loadingHint.style.display = 'none';
drawImage();
resetAll();
zoomScale = 1;
if (zoomSlider) zoomSlider.value = 1;
applyZoom();
saveBtn.disabled = false;
resetImageBtn.disabled = false;
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
});
operationMode.addEventListener('change', updateOptionsDisplay);
clearBtn.addEventListener('click', function(){
resetAll();
});
applyBtn.addEventListener('click', function(e){
e.preventDefault();
applyRegionsOperation();
});
saveBtn.addEventListener('click', function(e){
e.preventDefault();
saveImage();
});
resetImageBtn.addEventListener('click', function(e){
e.preventDefault();
if (imgLoaded) {
drawImage();
resetAll();
zoomScale = 1;
if (zoomSlider) zoomSlider.value = 1;
applyZoom();
flashSuccess(resetImageBtn, '↩ Original wiederhergestellt');
}
});
if (zoomSlider) {
zoomSlider.addEventListener('input', function(){
setZoom(parseFloat(this.value) || 1);
});
}
if (zoomInBtn) {
zoomInBtn.addEventListener('click', function(){
setZoom(zoomScale + 0.1);
});
}
if (zoomOutBtn) {
zoomOutBtn.addEventListener('click', function(){
setZoom(zoomScale - 0.1);
});
}
window.addEventListener('resize', redrawRegions);
updateOptionsDisplay();
})();
</script>
</body>
</html>
CSS-Code
Hier nix
JavaScript-Code
Hier nix
PHP-Code
Hier nix

© Copyright 2025 Volker Niederastroth

Besucherstatistik

Besucher online: 2

Besucher heute: 97

Besucher diese Woche: 511

Besucher diesen Monat: 5707

Gesamtbesucher: 16271

💬