Files
playbull/public/js/image-editor.js
Mike 94b6cc3be9 Initial PlayBull release with Trilogy Hub integration.
Includes homelab deploy tooling, image position/zoom manifest persistence, and full trading/casino stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 08:31:36 +02:00

199 lines
7.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = `
<div class="modal image-editor-modal">
<div class="handle"></div>
<div class="modal-head">
<h2>${preset.label} positionieren</h2>
<button type="button" class="modal-close" data-act="cancel">✕</button>
</div>
<p class="muted center image-editor-hint">So wird dein Bild überall angezeigt — ziehen & zoomen</p>
<div class="image-editor-preview-wrap">
<div class="image-editor-preview ${preset.cssClass} ${preset.previewClass || 'framed-image--avatar-xl'}" id="ieFrame">
<img id="ieImg" src="${src}" alt="" draggable="false" />
</div>
</div>
<div class="field image-editor-zoom-field">
<label>Zoom <span id="ieZoomVal">${state.zoom.toFixed(1)}×</span></label>
<input type="range" id="ieZoom" min="1" max="4" step="0.05" value="${state.zoom}" />
</div>
<div class="btn-row">
<button type="button" class="btn btn-ghost" data-act="reset">Zurücksetzen</button>
<button type="button" class="btn btn-accent" data-act="save">Speichern</button>
</div>
</div>`;
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 });
};
});
}