layouts schick gemacht und packagelimits weiter implementiert
This commit is contained in:
@@ -63,7 +63,7 @@ export function DesignerCanvas({
|
||||
fabricCanvasRef.current = null;
|
||||
}
|
||||
|
||||
const upperEl = canvas.upperCanvasEl as (HTMLElement & Record<string, unknown>) | undefined;
|
||||
const upperEl = canvas.upperCanvasEl as unknown as (HTMLElement & Record<string, unknown>) | undefined;
|
||||
if (upperEl) {
|
||||
if (upperEl.__canvas === canvas) {
|
||||
delete upperEl.__canvas;
|
||||
@@ -73,7 +73,7 @@ export function DesignerCanvas({
|
||||
}
|
||||
}
|
||||
|
||||
const lowerEl = canvas.lowerCanvasEl as (HTMLElement & Record<string, unknown>) | undefined;
|
||||
const lowerEl = canvas.lowerCanvasEl as unknown as (HTMLElement & Record<string, unknown>) | undefined;
|
||||
if (lowerEl) {
|
||||
if (lowerEl.__canvas === canvas) {
|
||||
delete lowerEl.__canvas;
|
||||
@@ -140,6 +140,9 @@ export function DesignerCanvas({
|
||||
selection: !readOnly,
|
||||
preserveObjectStacking: true,
|
||||
perPixelTargetFind: true,
|
||||
transparentCorners: true,
|
||||
cornerSize: 8,
|
||||
padding: readOnly ? 0 : 10, // Default padding for text/objects, 0 for readonly
|
||||
});
|
||||
|
||||
fabricCanvasRef.current = canvas;
|
||||
@@ -149,7 +152,7 @@ export function DesignerCanvas({
|
||||
(window as unknown as Record<string, unknown>).__inviteCanvas = canvas;
|
||||
(element as unknown as { __fabricCanvas?: fabric.Canvas }).__fabricCanvas = canvas;
|
||||
if (containerRef.current) {
|
||||
const wrapper = containerRef.current as (HTMLElement & Record<string, unknown>);
|
||||
const wrapper = containerRef.current as unknown as (HTMLElement & Record<string, unknown>);
|
||||
wrapper.__fabricCanvas = canvas;
|
||||
Object.defineProperty(wrapper, '__canvas', {
|
||||
configurable: true,
|
||||
@@ -214,33 +217,78 @@ export function DesignerCanvas({
|
||||
onSelect(null);
|
||||
};
|
||||
|
||||
const handleObjectModified = (event: fabric.IEvent<fabric.Object>) => {
|
||||
const handleObjectModified = (e: any) => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
const target = event.target as FabricObjectWithId | undefined;
|
||||
const target = e.target as FabricObjectWithId | undefined;
|
||||
if (!target || typeof target.elementId !== 'string') {
|
||||
return;
|
||||
}
|
||||
const elementId = target.elementId;
|
||||
|
||||
const bounds = target.getBoundingRect(true, true);
|
||||
const nextPatch: Partial<LayoutElement> = {
|
||||
x: clamp(Math.round(bounds.left ?? 0), 0, CANVAS_WIDTH),
|
||||
y: clamp(Math.round(bounds.top ?? 0), 0, CANVAS_HEIGHT),
|
||||
width: clamp(Math.round(bounds.width ?? target.width ?? 0), 40, CANVAS_WIDTH),
|
||||
height: clamp(Math.round(bounds.height ?? target.height ?? 0), 40, CANVAS_HEIGHT),
|
||||
const bounds = target.getBoundingRect();
|
||||
let nextPatch: Partial<LayoutElement> = {
|
||||
x: clamp(Math.round(bounds.left ?? 0), 20, CANVAS_WIDTH - 20),
|
||||
y: clamp(Math.round(bounds.top ?? 0), 20, CANVAS_HEIGHT - 20),
|
||||
};
|
||||
|
||||
target.set({
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
left: nextPatch.x,
|
||||
top: nextPatch.y,
|
||||
width: nextPatch.width,
|
||||
height: nextPatch.height,
|
||||
// Manual collision check: Calculate overlap and push vertically
|
||||
const otherObjects = canvas.getObjects().filter(obj => obj !== target && (obj as FabricObjectWithId).elementId);
|
||||
otherObjects.forEach(other => {
|
||||
const otherBounds = other.getBoundingRect();
|
||||
const overlapX = Math.max(0, Math.min(bounds.left + bounds.width, otherBounds.left + otherBounds.width) - Math.max(bounds.left, otherBounds.left));
|
||||
const overlapY = Math.max(0, Math.min(bounds.top + bounds.height, otherBounds.top + otherBounds.height) - Math.max(bounds.top, otherBounds.top));
|
||||
if (overlapX > 0 && overlapY > 0) {
|
||||
// Push down by 120px if overlap (massive spacing für größeren QR-Code)
|
||||
nextPatch.y = Math.max(nextPatch.y, (Number(otherBounds.top || 0)) + (Number(otherBounds.height || 0)) + 120);
|
||||
}
|
||||
});
|
||||
|
||||
const isImage = target.type === 'image';
|
||||
if (isImage) {
|
||||
const currentScaleX = target.scaleX ?? 1;
|
||||
const currentScaleY = target.scaleY ?? 1;
|
||||
const naturalWidth = target.width ?? 0;
|
||||
const naturalHeight = target.height ?? 0;
|
||||
if (elementId === 'qr') {
|
||||
// For QR: Enforce uniform scale, cap size, padding=0
|
||||
const avgScale = (currentScaleX + currentScaleY) / 2;
|
||||
const cappedSize = Math.min(Math.round(naturalWidth * avgScale), 800); // Cap at 800px for massive QR
|
||||
nextPatch.width = cappedSize;
|
||||
nextPatch.height = cappedSize;
|
||||
nextPatch.scaleX = cappedSize / naturalWidth;
|
||||
nextPatch.scaleY = cappedSize / naturalHeight;
|
||||
target.set({
|
||||
left: nextPatch.x,
|
||||
top: nextPatch.y,
|
||||
scaleX: nextPatch.scaleX,
|
||||
scaleY: nextPatch.scaleY,
|
||||
padding: 12, // Increased padding for better frame visibility
|
||||
uniformScaling: true, // Lock aspect ratio
|
||||
lockScalingFlip: true,
|
||||
});
|
||||
} else {
|
||||
nextPatch.width = Math.round(naturalWidth * currentScaleX);
|
||||
nextPatch.height = Math.round(naturalHeight * currentScaleY);
|
||||
nextPatch.scaleX = currentScaleX;
|
||||
nextPatch.scaleY = currentScaleY;
|
||||
target.set({ left: nextPatch.x, top: nextPatch.y, padding: 10 });
|
||||
}
|
||||
} else {
|
||||
nextPatch.width = clamp(Math.round(bounds.width ?? target.width ?? 0), 40, CANVAS_WIDTH - 40);
|
||||
nextPatch.height = clamp(Math.round(bounds.height ?? target.height ?? 0), 40, CANVAS_HEIGHT - 40);
|
||||
target.set({
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
left: nextPatch.x,
|
||||
top: nextPatch.y,
|
||||
width: nextPatch.width,
|
||||
height: nextPatch.height,
|
||||
padding: 10, // Default padding for text
|
||||
});
|
||||
}
|
||||
|
||||
onChange(elementId, nextPatch);
|
||||
canvas.requestRenderAll();
|
||||
};
|
||||
@@ -348,39 +396,15 @@ export function DesignerCanvas({
|
||||
|
||||
const normalizedScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
||||
|
||||
canvas.setZoom(normalizedScale);
|
||||
|
||||
const cssWidth = CANVAS_WIDTH * normalizedScale;
|
||||
const cssHeight = CANVAS_HEIGHT * normalizedScale;
|
||||
|
||||
const element = canvas.getElement();
|
||||
if (element) {
|
||||
element.style.width = `${cssWidth}px`;
|
||||
element.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
if (canvas.upperCanvasEl) {
|
||||
canvas.upperCanvasEl.style.width = `${cssWidth}px`;
|
||||
canvas.upperCanvasEl.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
if (canvas.lowerCanvasEl) {
|
||||
canvas.lowerCanvasEl.style.width = `${cssWidth}px`;
|
||||
canvas.lowerCanvasEl.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
if (canvas.wrapperEl) {
|
||||
canvas.wrapperEl.style.width = `${cssWidth}px`;
|
||||
canvas.wrapperEl.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
if (containerRef.current) {
|
||||
containerRef.current.style.width = `${cssWidth}px`;
|
||||
containerRef.current.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
canvas.calcOffset();
|
||||
canvas.viewportTransform = [normalizedScale, 0, 0, normalizedScale, 0, 0];
|
||||
canvas.setDimensions({
|
||||
width: CANVAS_WIDTH * normalizedScale,
|
||||
height: CANVAS_HEIGHT * normalizedScale,
|
||||
});
|
||||
canvas.requestRenderAll();
|
||||
canvas.calcViewportBoundaries();
|
||||
|
||||
console.log('Zoom applied:', normalizedScale, 'Transform:', canvas.viewportTransform);
|
||||
}, [scale]);
|
||||
|
||||
return (
|
||||
@@ -472,7 +496,7 @@ export async function renderFabricLayout(
|
||||
if (typeof object.setCoords === 'function') {
|
||||
object.setCoords();
|
||||
}
|
||||
const bounds = object.getBoundingRect(true, true);
|
||||
const bounds = object.getBoundingRect();
|
||||
console.warn('[Invites][Fabric] added object', {
|
||||
elementId: (object as FabricObjectWithId).elementId,
|
||||
left: bounds.left,
|
||||
@@ -495,7 +519,7 @@ export function applyBackground(
|
||||
color: string,
|
||||
gradient: { angle?: number; stops?: string[] } | null,
|
||||
): void {
|
||||
let background: string | fabric.Gradient = color;
|
||||
let background: string | fabric.Gradient<'linear'> = color;
|
||||
|
||||
if (gradient?.stops?.length) {
|
||||
const angle = ((gradient.angle ?? 180) * Math.PI) / 180;
|
||||
@@ -512,15 +536,15 @@ export function applyBackground(
|
||||
x2: halfWidth + x * halfWidth,
|
||||
y2: halfHeight + y * halfHeight,
|
||||
},
|
||||
colorStops: gradient.stops.map((stop, index) => ({
|
||||
offset: gradient.stops.length === 1 ? 0 : index / (gradient.stops.length - 1),
|
||||
colorStops: gradient.stops!.map((stop, index) => ({
|
||||
offset: gradient.stops!.length === 1 ? 0 : index / (gradient.stops!.length - 1),
|
||||
color: stop,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
const canvasWithBackgroundFn = canvas as fabric.Canvas & {
|
||||
setBackgroundColor?: (value: string | fabric.Gradient, callback?: () => void) => void;
|
||||
setBackgroundColor?: (value: string | fabric.Gradient<'linear'>, callback?: () => void) => void;
|
||||
};
|
||||
|
||||
if (typeof canvasWithBackgroundFn.setBackgroundColor === 'function') {
|
||||
@@ -578,9 +602,13 @@ export async function createFabricObject({
|
||||
...baseConfig,
|
||||
width: element.width,
|
||||
height: element.height,
|
||||
fontSize: element.fontSize ?? 26,
|
||||
fontSize: element.fontSize ?? 36,
|
||||
fill: textColor,
|
||||
fontFamily: element.fontFamily ?? 'Lora',
|
||||
textAlign: mapTextAlign(element.align),
|
||||
lineHeight: element.lineHeight ?? 1.5,
|
||||
charSpacing: element.letterSpacing ?? 0.5,
|
||||
padding: 12, // Enhanced padding for better readability
|
||||
});
|
||||
case 'link':
|
||||
return new fabric.Textbox(element.content ?? '', {
|
||||
@@ -589,8 +617,12 @@ export async function createFabricObject({
|
||||
height: element.height,
|
||||
fontSize: element.fontSize ?? 24,
|
||||
fill: accentColor,
|
||||
fontFamily: element.fontFamily ?? 'Montserrat',
|
||||
underline: true,
|
||||
textAlign: mapTextAlign(element.align),
|
||||
lineHeight: element.lineHeight ?? 1.5,
|
||||
charSpacing: element.letterSpacing ?? 0.5,
|
||||
padding: 10,
|
||||
});
|
||||
case 'badge':
|
||||
return createTextBadge({
|
||||
@@ -601,6 +633,8 @@ export async function createFabricObject({
|
||||
backgroundColor: badgeColor,
|
||||
textColor: '#ffffff',
|
||||
fontSize: element.fontSize ?? 22,
|
||||
lineHeight: element.lineHeight ?? 1.5,
|
||||
letterSpacing: element.letterSpacing ?? 0.5,
|
||||
});
|
||||
case 'cta':
|
||||
return createTextBadge({
|
||||
@@ -612,6 +646,8 @@ export async function createFabricObject({
|
||||
textColor: '#ffffff',
|
||||
fontSize: element.fontSize ?? 24,
|
||||
cornerRadius: 18,
|
||||
lineHeight: element.lineHeight ?? 1.5,
|
||||
letterSpacing: element.letterSpacing ?? 0.5,
|
||||
});
|
||||
case 'logo':
|
||||
if (logoDataUrl) {
|
||||
@@ -627,15 +663,28 @@ export async function createFabricObject({
|
||||
qrCodeDataUrl.length,
|
||||
qrCodeDataUrl.slice(0, 48),
|
||||
);
|
||||
return loadImageObject(qrCodeDataUrl, element, baseConfig, {
|
||||
const qrImage = await loadImageObject(qrCodeDataUrl, element, baseConfig, {
|
||||
shadow: 'rgba(15,23,42,0.25)',
|
||||
padding: 0, // No padding to fix large frame
|
||||
});
|
||||
if (qrImage) {
|
||||
(qrImage as any).uniformScaling = true; // Lock aspect ratio
|
||||
qrImage.lockScalingFlip = true;
|
||||
qrImage.padding = 0;
|
||||
qrImage.cornerColor = 'transparent';
|
||||
qrImage.borderScaleFactor = 1; // Prevent border inflation on scale
|
||||
}
|
||||
console.log('QR DataURL:', qrCodeDataUrl ? 'Loaded' : 'Fallback');
|
||||
return qrImage;
|
||||
}
|
||||
console.log('QR Fallback used - DataURL missing');
|
||||
return new fabric.Rect({
|
||||
...baseConfig,
|
||||
width: element.width,
|
||||
height: element.height,
|
||||
fill: secondaryColor,
|
||||
fill: 'white',
|
||||
stroke: secondaryColor,
|
||||
strokeWidth: 2,
|
||||
rx: 20,
|
||||
ry: 20,
|
||||
});
|
||||
@@ -646,6 +695,7 @@ export async function createFabricObject({
|
||||
height: element.height,
|
||||
fontSize: element.fontSize ?? 24,
|
||||
fill: secondaryColor,
|
||||
fontFamily: element.fontFamily ?? 'Lora',
|
||||
textAlign: mapTextAlign(element.align),
|
||||
});
|
||||
}
|
||||
@@ -660,6 +710,8 @@ export function createTextBadge({
|
||||
textColor,
|
||||
fontSize,
|
||||
cornerRadius = 12,
|
||||
lineHeight = 1.5,
|
||||
letterSpacing = 0.5,
|
||||
}: {
|
||||
baseConfig: FabricObjectWithId;
|
||||
text: string;
|
||||
@@ -669,6 +721,8 @@ export function createTextBadge({
|
||||
textColor: string;
|
||||
fontSize: number;
|
||||
cornerRadius?: number;
|
||||
lineHeight?: number;
|
||||
letterSpacing?: number;
|
||||
}): fabric.Group {
|
||||
const rect = new fabric.Rect({
|
||||
width,
|
||||
@@ -688,8 +742,11 @@ export function createTextBadge({
|
||||
top: height / 2,
|
||||
fontSize,
|
||||
fill: textColor,
|
||||
fontFamily: 'Montserrat',
|
||||
originY: 'center',
|
||||
textAlign: 'center',
|
||||
lineHeight,
|
||||
charSpacing: letterSpacing,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
});
|
||||
@@ -707,7 +764,7 @@ export async function loadImageObject(
|
||||
source: string,
|
||||
element: LayoutElement,
|
||||
baseConfig: FabricObjectWithId,
|
||||
options?: { objectFit?: 'contain' | 'cover'; shadow?: string },
|
||||
options?: { objectFit?: 'contain' | 'cover'; shadow?: string; padding?: number },
|
||||
): Promise<fabric.Object | null> {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
@@ -741,6 +798,7 @@ export async function loadImageObject(
|
||||
height: element.height,
|
||||
scaleX,
|
||||
scaleY,
|
||||
padding: options?.padding ?? 0,
|
||||
});
|
||||
|
||||
if (options?.shadow) {
|
||||
@@ -779,23 +837,18 @@ export async function loadImageObject(
|
||||
imageElement.onerror = onError;
|
||||
imageElement.src = source;
|
||||
} else {
|
||||
fabric.util.loadImage(
|
||||
source,
|
||||
(img) => {
|
||||
if (!img) {
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
console.debug('[Invites][Fabric] image loaded', {
|
||||
source: source.slice(0, 48),
|
||||
width: (img as HTMLImageElement).width,
|
||||
height: (img as HTMLImageElement).height,
|
||||
});
|
||||
onImageLoaded(img);
|
||||
},
|
||||
undefined,
|
||||
'anonymous',
|
||||
);
|
||||
// Use direct Image constructor approach for better compatibility
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
console.debug('[Invites][Fabric] image loaded', {
|
||||
source: source.slice(0, 48),
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
});
|
||||
onImageLoaded(img);
|
||||
};
|
||||
img.onerror = onError;
|
||||
img.src = source;
|
||||
}
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
|
||||
Reference in New Issue
Block a user