frame bg: make legacy scenes opt-in

This commit is contained in:
Ryan Di
2026-03-18 19:49:46 +11:00
parent 3f405ab833
commit 858d1d4cce
9 changed files with 76 additions and 3 deletions
+4 -1
View File
@@ -10,6 +10,7 @@ import {
getFontString, getFontString,
getUpdatedTimestamp, getUpdatedTimestamp,
getLineHeight, getLineHeight,
isTransparent,
} from "@excalidraw/common"; } from "@excalidraw/common";
import type { Radians } from "@excalidraw/math"; import type { Radians } from "@excalidraw/math";
@@ -185,11 +186,13 @@ export const newFrameElement = (
name?: string; name?: string;
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawFrameElement> => { ): NonDeleted<ExcalidrawFrameElement> => {
const frameBase = _newElementBase<ExcalidrawFrameElement>("frame", opts);
const frameElement = newElementWith( const frameElement = newElementWith(
{ {
..._newElementBase<ExcalidrawFrameElement>("frame", opts), ...frameBase,
type: "frame", type: "frame",
name: opts?.name || null, name: opts?.name || null,
backgroundEnabled: !isTransparent(frameBase.backgroundColor),
}, },
{}, {},
); );
+1 -1
View File
@@ -787,7 +787,7 @@ export const renderFrameBackground = (
roundCorners?: boolean; roundCorners?: boolean;
}, },
) => { ) => {
if (isTransparent(frame.backgroundColor)) { if (!frame.backgroundEnabled || isTransparent(frame.backgroundColor)) {
return; return;
} }
+1
View File
@@ -163,6 +163,7 @@ export type InitializedExcalidrawImageElement = MarkNonNullable<
export type ExcalidrawFrameElement = _ExcalidrawElementBase & { export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
type: "frame"; type: "frame";
name: string | null; name: string | null;
backgroundEnabled: boolean;
}; };
export type ExcalidrawMagicFrameElement = _ExcalidrawElementBase & { export type ExcalidrawMagicFrameElement = _ExcalidrawElementBase & {
@@ -417,7 +417,13 @@ export const actionChangeBackgroundColor = register<
}); });
} else { } else {
nextElements = changeProperty(elements, appState, (el) => { nextElements = changeProperty(elements, appState, (el) => {
return hasBackground(el.type) || isFrameElement(el) if (isFrameElement(el)) {
return newElementWith(el, {
backgroundColor: value.currentItemBackgroundColor,
backgroundEnabled: !isTransparent(value.currentItemBackgroundColor),
});
}
return hasBackground(el.type)
? newElementWith(el, { ? newElementWith(el, {
backgroundColor: value.currentItemBackgroundColor, backgroundColor: value.currentItemBackgroundColor,
}) })
+4
View File
@@ -505,9 +505,13 @@ export const restoreElement = (
case "embeddable": case "embeddable":
return restoreElementWithProperties(element, {}); return restoreElementWithProperties(element, {});
case "magicframe": case "magicframe":
return restoreElementWithProperties(element, {
name: element.name ?? null,
});
case "frame": case "frame":
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
name: element.name ?? null, name: element.name ?? null,
backgroundEnabled: (element as any).backgroundEnabled ?? false,
}); });
// Don't use default case so as to catch a missing an element type case. // Don't use default case so as to catch a missing an element type case.
@@ -336,6 +336,9 @@ const _renderStaticScene = ({
if (!isFrameElement(frame) || renderedFrameBackgrounds.has(frame.id)) { if (!isFrameElement(frame) || renderedFrameBackgrounds.has(frame.id)) {
return; return;
} }
if (!frame.backgroundEnabled) {
return;
}
frameBackgroundByElementId.set(element.id, frame); frameBackgroundByElementId.set(element.id, frame);
renderedFrameBackgrounds.add(frame.id); renderedFrameBackgrounds.add(frame.id);
@@ -731,6 +731,7 @@ export const renderSceneToSvg = (
const renderFrameBackgroundNode = (frame: ExcalidrawFrameElement) => { const renderFrameBackgroundNode = (frame: ExcalidrawFrameElement) => {
if ( if (
!frame || !frame ||
!frame.backgroundEnabled ||
!frame.backgroundColor || !frame.backgroundColor ||
isTransparent(frame.backgroundColor) isTransparent(frame.backgroundColor)
) { ) {
@@ -793,6 +794,7 @@ export const renderSceneToSvg = (
if ( if (
!isFrameElement(frame) || !isFrameElement(frame) ||
renderedFrameBackgrounds.has(frame.id) || renderedFrameBackgrounds.has(frame.id) ||
!frame.backgroundEnabled ||
!frame.backgroundColor || !frame.backgroundColor ||
isTransparent(frame.backgroundColor) isTransparent(frame.backgroundColor)
) { ) {
@@ -11,6 +11,7 @@ import type { LocalPoint } from "@excalidraw/math";
import type { import type {
ExcalidrawArrowElement, ExcalidrawArrowElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawFrameElement,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElement, ExcalidrawTextElement,
@@ -81,6 +82,38 @@ describe("restoreElements", () => {
).toEqual([expect.objectContaining({ isDeleted: true })]); ).toEqual([expect.objectContaining({ isDeleted: true })]);
}); });
it("should disable frame background for legacy frames missing backgroundEnabled", () => {
const frame = API.createElement({
type: "frame",
backgroundColor: "#ffc9c9",
});
const legacyFrame = { ...frame } as any;
delete legacyFrame.backgroundEnabled;
const restoredFrame = restore.restoreElements(
[legacyFrame],
null,
)[0] as ExcalidrawFrameElement;
expect(restoredFrame.backgroundColor).toBe("#ffc9c9");
expect(restoredFrame.backgroundEnabled).toBe(false);
});
it("should preserve frame backgroundEnabled when present", () => {
const frame = API.createElement({
type: "frame",
backgroundColor: "#ffc9c9",
});
frame.backgroundEnabled = true;
const restoredFrame = restore.restoreElements(
[frame],
null,
)[0] as ExcalidrawFrameElement;
expect(restoredFrame.backgroundEnabled).toBe(true);
});
it("should restore text element correctly passing value for each attribute", () => { it("should restore text element correctly passing value for each attribute", () => {
const textElement = API.createElement({ const textElement = API.createElement({
type: "text", type: "text",
@@ -364,6 +364,27 @@ describe("exporting frames", () => {
expect(emptyBgSvg.querySelector('rect[fill=""]')).toBeNull(); expect(emptyBgSvg.querySelector('rect[fill=""]')).toBeNull();
}); });
it("should not render SVG frame background when background is disabled", async () => {
const frame = API.createElement({
type: "frame",
width: 100,
height: 100,
x: 0,
y: 0,
backgroundColor: "#ffc9c9",
});
frame.backgroundEnabled = false;
const svg = await exportToSvg({
elements: [frame],
files: null,
exportPadding: 0,
exportingFrame: frame,
});
expect(svg.querySelector('rect[fill="#ffc9c9"]')).toBeNull();
});
it("should filter non-overlapping elements when exporting a frame", async () => { it("should filter non-overlapping elements when exporting a frame", async () => {
const frame = API.createElement({ const frame = API.createElement({
type: "frame", type: "frame",