Enable foldable background presets
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-24 09:02:52 +01:00
parent 6bd75b0788
commit e3b356e810
3 changed files with 153 additions and 40 deletions

View File

@@ -23,6 +23,7 @@ type DesignerCanvasProps = {
qrCodeDataUrl: string | null;
logoDataUrl: string | null;
backgroundImageUrl?: string | null;
backgroundImagePanels?: BackgroundImagePanel[];
scale?: number;
readOnly?: boolean;
layoutKey?: string;
@@ -38,6 +39,7 @@ export function DesignerCanvas({
background,
gradient,
backgroundImageUrl = null,
backgroundImagePanels,
accent,
text,
secondary,
@@ -348,6 +350,7 @@ export function DesignerCanvas({
background,
gradient,
backgroundImageUrl,
backgroundImagePanels,
readOnly,
});
@@ -367,6 +370,7 @@ export function DesignerCanvas({
backgroundColor: background,
backgroundGradient: gradient,
backgroundImageUrl,
backgroundImagePanels,
readOnly,
}).catch((error) => {
console.error('[Fabric] Failed to render layout', error);
@@ -382,6 +386,7 @@ export function DesignerCanvas({
background,
gradient,
backgroundImageUrl,
backgroundImagePanels,
readOnly,
]);
@@ -458,9 +463,20 @@ export type FabricRenderOptions = {
backgroundColor: string;
backgroundGradient: { angle?: number; stops?: string[] } | null;
backgroundImageUrl?: string | null;
backgroundImagePanels?: BackgroundImagePanel[];
readOnly: boolean;
};
export type BackgroundImagePanel = {
url: string;
centerX: number;
centerY: number;
width: number;
height: number;
rotation: number;
mirrored?: boolean;
};
export async function renderFabricLayout(
canvas: fabric.Canvas,
options: FabricRenderOptions,
@@ -476,6 +492,7 @@ export async function renderFabricLayout(
backgroundColor,
backgroundGradient,
backgroundImageUrl,
backgroundImagePanels,
readOnly,
} = options;
@@ -489,7 +506,7 @@ export async function renderFabricLayout(
}
canvas.clear();
await applyBackground(canvas, backgroundColor, backgroundGradient, backgroundImageUrl);
await applyBackground(canvas, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundImagePanels);
console.debug('[Invites][Fabric] render', {
elementCount: elements.length,
@@ -558,6 +575,7 @@ export async function applyBackground(
color: string,
gradient: { angle?: number; stops?: string[] } | null,
backgroundImageUrl?: string | null,
backgroundImagePanels?: BackgroundImagePanel[],
): Promise<void> {
try {
if (typeof canvas.setBackgroundImage === 'function') {
@@ -568,25 +586,47 @@ export async function applyBackground(
canvas.requestRenderAll();
}
if (backgroundImagePanels?.length) {
applyBackgroundFill(canvas, color, gradient);
const panelImages = await Promise.all(
backgroundImagePanels.map(async (panel) => ({
panel,
element: await loadFabricImage(resolveAssetUrl(panel.url)),
})),
);
panelImages.forEach(({ panel, element }) => {
if (!element) {
return;
}
const needsSwap = Math.abs(panel.rotation) % 180 === 90;
const targetWidth = needsSwap ? panel.height : panel.width;
const targetHeight = needsSwap ? panel.width : panel.height;
const scale = Math.max(targetWidth / (element.width || targetWidth), targetHeight / (element.height || targetHeight));
const scaleX = panel.mirrored ? -scale : scale;
const scaleY = scale;
element.set({
originX: 'center',
originY: 'center',
left: panel.centerX,
top: panel.centerY,
scaleX,
scaleY,
angle: panel.rotation,
selectable: false,
evented: false,
});
canvas.add(element);
});
canvas.requestRenderAll();
return;
}
if (backgroundImageUrl) {
try {
const resolvedUrl = backgroundImageUrl.startsWith('http')
? backgroundImageUrl
: `${window.location.origin}${backgroundImageUrl.startsWith('/') ? '' : '/'}${backgroundImageUrl}`;
const image = await new Promise<fabric.Image | null>((resolve) => {
const imgEl = new Image();
imgEl.crossOrigin = 'anonymous';
const timeoutId = window.setTimeout(() => resolve(null), 3000);
imgEl.onload = () => {
window.clearTimeout(timeoutId);
resolve(new fabric.Image(imgEl, { crossOrigin: 'anonymous' }));
};
imgEl.onerror = () => {
window.clearTimeout(timeoutId);
resolve(null);
};
imgEl.src = resolvedUrl;
});
const image = await loadFabricImage(resolveAssetUrl(backgroundImageUrl));
if (image) {
const scaleX = CANVAS_WIDTH / (image.width || CANVAS_WIDTH);
const scaleY = CANVAS_HEIGHT / (image.height || CANVAS_HEIGHT);
@@ -617,6 +657,14 @@ export async function applyBackground(
console.warn('[Fabric] applyBackground failed', error);
}
applyBackgroundFill(canvas, color, gradient);
}
function applyBackgroundFill(
canvas: fabric.Canvas,
color: string,
gradient: { angle?: number; stops?: string[] } | null,
): void {
let background: string | fabric.Gradient<'linear'> = color;
if (gradient?.stops?.length) {
@@ -653,6 +701,30 @@ export async function applyBackground(
}
}
async function loadFabricImage(url: string): Promise<fabric.Image | null> {
return new Promise<fabric.Image | null>((resolve) => {
const imgEl = new Image();
imgEl.crossOrigin = 'anonymous';
const timeoutId = window.setTimeout(() => resolve(null), 3000);
imgEl.onload = () => {
window.clearTimeout(timeoutId);
resolve(new fabric.Image(imgEl, { crossOrigin: 'anonymous' }));
};
imgEl.onerror = () => {
window.clearTimeout(timeoutId);
resolve(null);
};
imgEl.src = url;
});
}
function resolveAssetUrl(url: string): string {
if (url.startsWith('http')) {
return url;
}
return `${window.location.origin}${url.startsWith('/') ? '' : '/'}${url}`;
}
export type FabricObjectFactoryContext = {
element: LayoutElement;
accentColor: string;