// Gemeinsame Transform-Logik fΓΌr Editor-Vorschau und finale Anzeige export const FRAMES = { avatar: { id: 'avatar', aspectRatio: 1, label: 'Profilbild', cssClass: 'framed-image framed-image--avatar', previewClass: 'framed-image--avatar-xl', sizes: { sm: 'framed-image--avatar-sm', md: 'framed-image--avatar-md', lg: 'framed-image--avatar-lg' }, }, gallery: { id: 'gallery', aspectRatio: 1, label: 'Quadrat', cssClass: 'framed-image framed-image--gallery', previewClass: 'framed-image--gallery-xl', sizes: { sm: 'framed-image--gallery-sm', md: 'framed-image--gallery-md', lg: 'framed-image--gallery-lg' }, }, banner: { id: 'banner', aspectRatio: 3, label: 'Banner', cssClass: 'framed-image framed-image--banner', previewClass: 'framed-image--banner-xl', sizes: { sm: 'framed-image--banner-sm', md: 'framed-image--banner-md', lg: 'framed-image--banner-lg' }, }, card: { id: 'card', aspectRatio: 2 / 3, label: 'Karte', cssClass: 'framed-image framed-image--card', previewClass: 'framed-image--card-xl', sizes: { sm: 'framed-image--card-sm', md: 'framed-image--card-md', lg: 'framed-image--card-lg' }, }, }; export function computeFramedLayout(imgW, imgH, frameW, frameH, focus, zoom) { const fx = focus?.x ?? 0.5; const fy = focus?.y ?? 0.5; const z = Math.max(1, zoom ?? 1); const cover = Math.max(frameW / imgW, frameH / imgH); const scale = cover * z; const w = imgW * scale; const h = imgH * scale; let left = frameW / 2 - fx * w; let top = frameH / 2 - fy * h; left = Math.min(0, Math.max(frameW - w, left)); top = Math.min(0, Math.max(frameH - h, top)); return { width: w, height: h, left, top, scale }; } export function focusFromLayout(imgW, imgH, frameW, frameH, layout, zoom) { const z = Math.max(1, zoom ?? 1); const cover = Math.max(frameW / imgW, frameH / imgH); const scale = cover * z; const w = imgW * scale; const h = imgH * scale; return { x: Math.min(1, Math.max(0, (frameW / 2 - layout.left) / w)), y: Math.min(1, Math.max(0, (frameH / 2 - layout.top) / h)), }; } export function applyFramedLayout(imgEl, layout) { imgEl.style.width = `${layout.width}px`; imgEl.style.height = `${layout.height}px`; imgEl.style.left = `${layout.left}px`; imgEl.style.top = `${layout.top}px`; } export function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('Bild konnte nicht geladen werden')); img.src = src; }); } function sizeClass(frame, size) { const preset = FRAMES[frame] || FRAMES.gallery; return preset.sizes[size] || preset.sizes.sm; } export function isImageAvatar(avatar) { return avatar && typeof avatar === 'object' && avatar.type === 'image' && avatar.url; } export function avatarEmoji(avatar) { if (!avatar) return 'πŸ‚'; if (typeof avatar === 'string') return avatar; if (isImageAvatar(avatar)) return avatar.emoji || 'πŸ‚'; return avatar.value || 'πŸ‚'; } export function renderFramedImage({ url, focus, zoom, frame = 'gallery', size = 'sm', alt = '' }) { const preset = FRAMES[frame] || FRAMES.gallery; const fx = focus?.x ?? 0.5; const fy = focus?.y ?? 0.5; const z = zoom ?? 1; return `
${escapeAttr(alt)}
`; } export function renderUploadItem(item) { return `
${renderFramedImage({ url: item.url, focus: item.focus, zoom: item.zoom, frame: item.frame, size: 'md' })}
`; } export function renderAvatar(avatar, { size = 'sm', alt = '' } = {}) { if (isImageAvatar(avatar)) { return renderFramedImage({ url: avatar.url, focus: avatar.focus, zoom: avatar.zoom, frame: 'avatar', size, alt, }); } const emoji = avatarEmoji(avatar); const cls = size === 'lg' ? 'avatar-emoji avatar-emoji--lg' : size === 'md' ? 'avatar-emoji avatar-emoji--md' : 'avatar-emoji avatar-emoji--sm'; return ``; } export function hydrateFramedImages(root = document) { root.querySelectorAll('.framed-image-host[data-framed]').forEach((host) => { const img = host.querySelector('img'); if (!img) return; const apply = () => { const layout = computeFramedLayout( img.naturalWidth, img.naturalHeight, host.clientWidth, host.clientHeight, { x: +host.dataset.fx, y: +host.dataset.fy }, +host.dataset.zoom, ); applyFramedLayout(img, layout); }; if (img.complete && img.naturalWidth) apply(); else img.addEventListener('load', apply, { once: true }); }); } function escapeAttr(s) { return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/