import { FRAMES, computeFramedLayout, focusFromLayout, applyFramedLayout, loadImage, } from './framed-image.js'; /** * Öffnet den Positions-Editor nach Dateiauswahl (oder für bestehendes Bild). * @returns {Promise<{focus:{x,y}, zoom:number}|null>} */ export async function openImagePositionEditor({ file, url, focus, zoom, frame = 'avatar' } = {}) { const preset = FRAMES[frame] || FRAMES.avatar; let src = url; if (file) src = URL.createObjectURL(file); let img; try { img = await loadImage(src); } catch (e) { if (file) URL.revokeObjectURL(src); throw e; } const state = { focus: { x: focus?.x ?? 0.5, y: focus?.y ?? 0.5 }, zoom: Math.max(1, zoom ?? 1), img, dragging: false, lastX: 0, lastY: 0, pinchDist: 0, pinchZoom: 1, }; return new Promise((resolve) => { const backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop image-editor-backdrop'; backdrop.innerHTML = ` `; document.body.appendChild(backdrop); document.body.style.overflow = 'hidden'; const frameEl = backdrop.querySelector('#ieFrame'); const imgEl = backdrop.querySelector('#ieImg'); const zoomInput = backdrop.querySelector('#ieZoom'); const zoomVal = backdrop.querySelector('#ieZoomVal'); let layout = { left: 0, top: 0, width: 0, height: 0, scale: 1 }; function syncFromFocus() { const fw = frameEl.clientWidth; const fh = frameEl.clientHeight; layout = computeFramedLayout(img.naturalWidth, img.naturalHeight, fw, fh, state.focus, state.zoom); applyFramedLayout(imgEl, layout); } function syncFocusFromLayout() { const fw = frameEl.clientWidth; const fh = frameEl.clientHeight; state.focus = focusFromLayout(img.naturalWidth, img.naturalHeight, fw, fh, layout, state.zoom); } function finish(result) { document.body.style.overflow = ''; backdrop.remove(); if (file) URL.revokeObjectURL(src); resolve(result); } function onPointerDown(e) { if (e.target.closest('button, input')) return; state.dragging = true; state.lastX = e.clientX; state.lastY = e.clientY; frameEl.setPointerCapture(e.pointerId); e.preventDefault(); } function onPointerMove(e) { if (!state.dragging) return; const dx = e.clientX - state.lastX; const dy = e.clientY - state.lastY; state.lastX = e.clientX; state.lastY = e.clientY; const fw = frameEl.clientWidth; const fh = frameEl.clientHeight; layout.left = Math.min(0, Math.max(fw - layout.width, layout.left + dx)); layout.top = Math.min(0, Math.max(fh - layout.height, layout.top + dy)); applyFramedLayout(imgEl, layout); syncFocusFromLayout(); } function onPointerUp(e) { state.dragging = false; try { frameEl.releasePointerCapture(e.pointerId); } catch { /* ignore */ } } function onWheel(e) { e.preventDefault(); const delta = e.deltaY > 0 ? -0.08 : 0.08; setZoom(state.zoom + delta); } function setZoom(z) { const fw = frameEl.clientWidth; const fh = frameEl.clientHeight; const oldZoom = state.zoom; state.zoom = Math.min(4, Math.max(1, z)); const oldLayout = { ...layout }; layout = computeFramedLayout(img.naturalWidth, img.naturalHeight, fw, fh, state.focus, state.zoom); // Fokuspunkt in der Mitte halten beim Zoomen const cx = fw / 2; const cy = fh / 2; const relX = (cx - oldLayout.left) / oldLayout.width; const relY = (cy - oldLayout.top) / oldLayout.height; layout.left = cx - relX * layout.width; layout.top = cy - relY * layout.height; layout.left = Math.min(0, Math.max(fw - layout.width, layout.left)); layout.top = Math.min(0, Math.max(fh - layout.height, layout.top)); syncFocusFromLayout(); applyFramedLayout(imgEl, layout); zoomInput.value = state.zoom; zoomVal.textContent = `${state.zoom.toFixed(1)}×`; } imgEl.addEventListener('load', syncFromFocus, { once: true }); if (imgEl.complete) syncFromFocus(); else requestAnimationFrame(syncFromFocus); frameEl.addEventListener('pointerdown', onPointerDown); frameEl.addEventListener('pointermove', onPointerMove); frameEl.addEventListener('pointerup', onPointerUp); frameEl.addEventListener('pointercancel', onPointerUp); frameEl.addEventListener('wheel', onWheel, { passive: false }); // Pinch-Zoom auf Touch frameEl.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { state.pinchDist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY, ); state.pinchZoom = state.zoom; } }, { passive: true }); frameEl.addEventListener('touchmove', (e) => { if (e.touches.length === 2 && state.pinchDist > 0) { e.preventDefault(); const dist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY, ); setZoom(state.pinchZoom * (dist / state.pinchDist)); } }, { passive: false }); zoomInput.addEventListener('input', () => setZoom(+zoomInput.value)); backdrop.addEventListener('click', (e) => { if (e.target === backdrop) finish(null); }); backdrop.querySelector('[data-act="cancel"]').onclick = () => finish(null); backdrop.querySelector('[data-act="reset"]').onclick = () => { state.focus = { x: 0.5, y: 0.5 }; state.zoom = 1; zoomInput.value = 1; zoomVal.textContent = '1.0×'; syncFromFocus(); }; backdrop.querySelector('[data-act="save"]').onclick = () => { syncFocusFromLayout(); finish({ focus: { ...state.focus }, zoom: state.zoom }); }; }); }