/** * xframe-crop — reine Geometrie des XFrame-Ausschnitts (quick-260922-ge2), * ohne React, fuer Kachel UND Vorschau. * * Einpassen (contain) und Zentrieren: der Ausschnitt muss in der Kachel * immer GANZ sichtbar sein, und das Seitenverhaeltnis der Kachel stimmt * praktisch nie mit dem des Ausschnitts ueberein — deshalb der kleinere * der beiden Massstaebe (darf > 1 sein: ein kleiner Ausschnitt in einer * grossen Kachel wird vergroessert) und die Zentrierung im Rest. * * `frameHeight` ist IMMER die Vorschauhoehe (3000 Seitenpixel), nicht * "Unterkante des Ausschnitts": viele Seiten setzen Masse relativ zur * Fensterhoehe (`vh`, `100vh`-Zentrierung, `margin: 15vh` bei example.com). * Haetten Vorschau und Kachel verschiedene Rahmenhoehen, laege derselbe * Inhalt an verschiedenen Stellen — der in der Vorschau gewaehlte Ausschnitt * zeigte in der Kachel etwas anderes (Browser-Rundgang 22.09.2026: Ueberschrift * in der Vorschau bei y 450, in der Kachel mit 720 px Rahmen bei y 108). * Gleiche Hoehe = gleiches Layout; der Rest wird ohnehin weggeschnitten. * * Ziehmathematik der Vorschau: `move` verschiebt bei fester Groesse und * klemmt an den Seitenraendern; die Eckmodi bewegen nur die zwei Kanten * ihrer Ecke — die gegenueberliegende Ecke bleibt stehen, damit sich der * Rahmen so verhaelt, wie man es von jedem Bildeditor kennt; die * Mindestgroesse wird an der BEWEGTEN Kante gehalten. Das Ergebnis geht * durch `clampXframeCrop` (eine Klemmregel fuer alles, T-GE2-05). */ import { XFRAME_CROP_MIN_H, XFRAME_CROP_MIN_W, XFRAME_PAGE_WIDTH, type XframeCrop, clampXframeCrop, } from './xframe-config'; /** * Hoehe der Seite in Seitenpixeln — in der Vorschau (Stage, Ziehen) UND als * Layouthoehe des Rahmens in der Kachel (siehe Kopfkommentar); 4000 bleibt * die Speichergrenze fuer `crop.y + crop.h`. */ export const XFRAME_PREVIEW_PAGE_HEIGHT = 3000; export type XframeDragMode = 'move' | 'nw' | 'ne' | 'sw' | 'se'; export interface XframeCropLayout { /** 0 = Kachel noch nicht gemessen -> nichts rendern. */ scale: number; left: number; top: number; frameHeight: number; } export function computeCropLayout(crop: XframeCrop, tileW: number, tileH: number): XframeCropLayout { const frameHeight = XFRAME_PREVIEW_PAGE_HEIGHT; if (tileW <= 0 || tileH <= 0) return { scale: 0, left: 0, top: 0, frameHeight }; const scale = Math.min(tileW / crop.w, tileH / crop.h); return { scale, left: (tileW - crop.w * scale) / 2, top: (tileH - crop.h * scale) / 2, frameHeight, }; } export function applyCropDrag(mode: XframeDragMode, start: XframeCrop, dxPage: number, dyPage: number): XframeCrop { let left = start.x; let top = start.y; let right = start.x + start.w; let bottom = start.y + start.h; if (mode === 'move') { left = Math.min(Math.max(0, left + dxPage), XFRAME_PAGE_WIDTH - start.w); top = Math.min(Math.max(0, top + dyPage), Math.max(0, XFRAME_PREVIEW_PAGE_HEIGHT - start.h)); return clampXframeCrop({ x: left, y: top, w: start.w, h: start.h }); } if (mode === 'nw' || mode === 'sw') { left = Math.min(Math.max(0, left + dxPage), right - XFRAME_CROP_MIN_W); } else { right = Math.max(Math.min(XFRAME_PAGE_WIDTH, right + dxPage), left + XFRAME_CROP_MIN_W); } if (mode === 'nw' || mode === 'ne') { top = Math.min(Math.max(0, top + dyPage), bottom - XFRAME_CROP_MIN_H); } else { bottom = Math.max(Math.min(XFRAME_PREVIEW_PAGE_HEIGHT, bottom + dyPage), top + XFRAME_CROP_MIN_H); } return clampXframeCrop({ x: left, y: top, w: right - left, h: bottom - top }); }