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 ;-)
Hier nix
<!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 & bearbeiten</h2><small>Bei Zoom >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 & 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">© 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ühlpendingTimer = setTimeout(() => {if (autoAction === 'pending') {autoAction = 'pan';// Optional: Zeige Indikator anif (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 6pxif (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>
Hier nix
Hier nix
Hier nix