fix order in blur layers + fix aligment
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import type Konva from "konva";
|
import type Konva from "konva";
|
||||||
import type { EditorObject } from "../../store/editorStore";
|
import type { EditorObject } from "../../store/editorStore";
|
||||||
import { getColorWithOpacity } from "../../utils/colorOpacity";
|
import {
|
||||||
import { toCssGradientAngle, type LinearGradient } from "../../utils/gradient";
|
buildBlurOverlayStyle,
|
||||||
|
buildBlurStackEntries,
|
||||||
|
isBlurBackdropObject,
|
||||||
|
} from "../../utils/blurOverlayLayout";
|
||||||
|
import OcclusionObjectsStage from "./OcclusionObjectsStage";
|
||||||
|
|
||||||
type BlurBackdropLayerProps = {
|
type BlurBackdropLayerProps = {
|
||||||
objects: EditorObject[];
|
objects: EditorObject[];
|
||||||
@@ -13,11 +17,9 @@ type BlurBackdropLayerProps = {
|
|||||||
stageRef: React.RefObject<Konva.Stage | null>;
|
stageRef: React.RefObject<Konva.Stage | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TRIANGLE_TOP_Y_PERCENT = 6.69873; // (1 - sqrt(3)/2) / 2
|
|
||||||
const TRIANGLE_BOTTOM_Y_PERCENT = 93.30127; // 100 - TRIANGLE_TOP_Y_PERCENT
|
|
||||||
|
|
||||||
const BlurBackdropLayer = ({ objects, stageWidth, stageHeight, scale, stageRef }: BlurBackdropLayerProps) => {
|
const BlurBackdropLayer = ({ objects, stageWidth, stageHeight, scale, stageRef }: BlurBackdropLayerProps) => {
|
||||||
const [dragRevision, setDragRevision] = useState(0);
|
const [dragRevision, setDragRevision] = useState(0);
|
||||||
|
const [draggedBlurObjectId, setDraggedBlurObjectId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stage = stageRef.current;
|
const stage = stageRef.current;
|
||||||
@@ -34,147 +36,66 @@ const BlurBackdropLayer = ({ objects, stageWidth, stageHeight, scale, stageRef }
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragMove = () => scheduleBump();
|
const resolveBlurObjectId = (nodeId: string): string | null => {
|
||||||
const onDragEnd = () => scheduleBump();
|
if (!nodeId) return null;
|
||||||
|
const direct = objects.find((obj) => obj.id === nodeId && isBlurBackdropObject(obj));
|
||||||
|
if (direct) return direct.id;
|
||||||
|
if (nodeId.startsWith("masked-")) {
|
||||||
|
const maskedId = nodeId.slice("masked-".length);
|
||||||
|
const masked = objects.find((obj) => obj.id === maskedId && isBlurBackdropObject(obj));
|
||||||
|
if (masked) return masked.id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStart = (event: Konva.KonvaEventObject<DragEvent>) => {
|
||||||
|
const target = event.target;
|
||||||
|
const id = typeof target.id === "function" ? target.id() : "";
|
||||||
|
const blurObjectId = resolveBlurObjectId(id);
|
||||||
|
if (blurObjectId) {
|
||||||
|
setDraggedBlurObjectId(blurObjectId);
|
||||||
|
}
|
||||||
|
scheduleBump();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragMove = () => scheduleBump();
|
||||||
|
|
||||||
|
const onDragEnd = () => {
|
||||||
|
setDraggedBlurObjectId(null);
|
||||||
|
scheduleBump();
|
||||||
|
};
|
||||||
|
|
||||||
|
stage.on("dragstart", onDragStart);
|
||||||
stage.on("dragmove", onDragMove);
|
stage.on("dragmove", onDragMove);
|
||||||
stage.on("dragend", onDragEnd);
|
stage.on("dragend", onDragEnd);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
stage.off("dragstart", onDragStart);
|
||||||
stage.off("dragmove", onDragMove);
|
stage.off("dragmove", onDragMove);
|
||||||
stage.off("dragend", onDragEnd);
|
stage.off("dragend", onDragEnd);
|
||||||
if (raf) window.cancelAnimationFrame(raf);
|
if (raf) window.cancelAnimationFrame(raf);
|
||||||
};
|
};
|
||||||
}, [stageRef]);
|
}, [objects, stageRef]);
|
||||||
|
|
||||||
|
const stackEntries = useMemo(() => buildBlurStackEntries(objects), [objects]);
|
||||||
|
|
||||||
|
const livePositions = useMemo(() => {
|
||||||
|
if (!draggedBlurObjectId) return new Map<string, { x: number; y: number }>();
|
||||||
|
|
||||||
const overlays = useMemo(() => {
|
|
||||||
const stage = stageRef.current;
|
const stage = stageRef.current;
|
||||||
|
if (!stage) return new Map<string, { x: number; y: number }>();
|
||||||
|
|
||||||
return objects
|
const node =
|
||||||
.map((obj, index) => {
|
stage.findOne(`#${draggedBlurObjectId}`) ??
|
||||||
if (obj.visible === false) return null;
|
stage.findOne(`#masked-${draggedBlurObjectId}`);
|
||||||
if (obj.isMask) return null;
|
if (node && "getAbsolutePosition" in node) {
|
||||||
if (obj.type !== "rectangle") return null;
|
return new Map([[draggedBlurObjectId, node.getAbsolutePosition()]]);
|
||||||
|
}
|
||||||
|
|
||||||
const blur = obj.blur ?? 0;
|
return new Map<string, { x: number; y: number }>();
|
||||||
if (blur <= 0) return null;
|
}, [draggedBlurObjectId, dragRevision, stageRef]);
|
||||||
|
|
||||||
const blurPx = blur * scale;
|
if (stackEntries.length === 0) return null;
|
||||||
const rotation = obj.rotation ?? 0;
|
|
||||||
const shapeType = obj.shapeType ?? "square";
|
|
||||||
|
|
||||||
// Konva shapes are positioned differently depending on shapeType.
|
|
||||||
let leftPx = 0;
|
|
||||||
let topPx = 0;
|
|
||||||
let widthPx = (obj.width ?? 100) * scale;
|
|
||||||
let heightPx = (obj.height ?? 100) * scale;
|
|
||||||
let borderRadiusPx = 0;
|
|
||||||
let clipPath: string | undefined;
|
|
||||||
let transformOrigin: CSSProperties["transformOrigin"] = "top left";
|
|
||||||
|
|
||||||
const node = stage?.findOne(`#${obj.id}`);
|
|
||||||
const absPos = node && "getAbsolutePosition" in node ? node.getAbsolutePosition() : null;
|
|
||||||
const nodeX = absPos?.x ?? node?.x?.() ?? obj.x ?? 0;
|
|
||||||
const nodeY = absPos?.y ?? node?.y?.() ?? obj.y ?? 0;
|
|
||||||
|
|
||||||
if (shapeType === "circle") {
|
|
||||||
const sizePx = (obj.width ?? 100) * scale;
|
|
||||||
const radiusPx = sizePx / 2;
|
|
||||||
// `getAbsolutePosition()` is already in the same pixel space as our overlay container.
|
|
||||||
// Konva.Circle uses x/y as CENTER coordinates, while our overlay uses top-left.
|
|
||||||
leftPx = nodeX - radiusPx;
|
|
||||||
topPx = nodeY - radiusPx;
|
|
||||||
widthPx = sizePx;
|
|
||||||
heightPx = sizePx;
|
|
||||||
borderRadiusPx = radiusPx;
|
|
||||||
transformOrigin = "center center";
|
|
||||||
} else if (shapeType === "triangle") {
|
|
||||||
const baseWidth = obj.width ?? 100;
|
|
||||||
const baseHeight = obj.height ?? 100;
|
|
||||||
const side = Math.min(baseWidth, baseHeight);
|
|
||||||
const radius = side / 2;
|
|
||||||
const radiusPx = radius * scale;
|
|
||||||
// In Konva TriangleShape, x/y are center coordinates.
|
|
||||||
leftPx = nodeX - radiusPx;
|
|
||||||
topPx = nodeY - radiusPx;
|
|
||||||
widthPx = side * scale;
|
|
||||||
heightPx = side * scale;
|
|
||||||
clipPath = `polygon(50% ${TRIANGLE_TOP_Y_PERCENT}%, 0% ${TRIANGLE_BOTTOM_Y_PERCENT}%, 100% ${TRIANGLE_BOTTOM_Y_PERCENT}%)`;
|
|
||||||
transformOrigin = "center center";
|
|
||||||
} else if (shapeType === "abstract") {
|
|
||||||
const rawW = obj.width ?? 100;
|
|
||||||
const rawH = obj.height ?? 100;
|
|
||||||
const width = rawW * scale;
|
|
||||||
const height = rawH * scale;
|
|
||||||
|
|
||||||
const baseRadius = Math.min(width, height) / 2;
|
|
||||||
const abstractScale = 0.85;
|
|
||||||
const radius = baseRadius * abstractScale;
|
|
||||||
|
|
||||||
// In Konva AbstractShape, x/y are center coordinates.
|
|
||||||
const centerX = nodeX;
|
|
||||||
const centerY = nodeY;
|
|
||||||
leftPx = centerX - radius;
|
|
||||||
topPx = centerY - radius;
|
|
||||||
widthPx = radius * 2;
|
|
||||||
heightPx = radius * 2;
|
|
||||||
clipPath =
|
|
||||||
"polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)";
|
|
||||||
transformOrigin = "center center";
|
|
||||||
} else {
|
|
||||||
// rectangle / square (Konva Rect uses x/y as top-left).
|
|
||||||
leftPx = nodeX;
|
|
||||||
topPx = nodeY;
|
|
||||||
widthPx = (obj.width ?? 100) * scale;
|
|
||||||
heightPx = (obj.height ?? 100) * scale;
|
|
||||||
borderRadiusPx = Math.max(0, obj.borderRadius ?? 0) * scale;
|
|
||||||
transformOrigin = "0% 0%";
|
|
||||||
}
|
|
||||||
|
|
||||||
const objOpacity = obj.opacity ?? 100; // 0-100 (editor convention)
|
|
||||||
// Make blur tint translucent so the backdrop blur stays visible.
|
|
||||||
const BLUR_TINT_OPACITY_FACTOR = 0.25;
|
|
||||||
const tintOpacity = Math.max(0, Math.min(100, objOpacity * BLUR_TINT_OPACITY_FACTOR));
|
|
||||||
const defaultFill = "#3b82f6";
|
|
||||||
|
|
||||||
const overlayBackground: CSSProperties =
|
|
||||||
obj.fillType === "gradient" && obj.gradient
|
|
||||||
? (() => {
|
|
||||||
const g = obj.gradient as LinearGradient;
|
|
||||||
const angle = toCssGradientAngle(g.angle);
|
|
||||||
const from = getColorWithOpacity(g.from, tintOpacity);
|
|
||||||
const to = getColorWithOpacity(g.to, tintOpacity);
|
|
||||||
return {
|
|
||||||
backgroundImage: `linear-gradient(${angle}deg, ${from} 0%, ${to} 100%)`,
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
};
|
|
||||||
})()
|
|
||||||
: {
|
|
||||||
backgroundColor: getColorWithOpacity(obj.fill ?? defaultFill, tintOpacity),
|
|
||||||
};
|
|
||||||
|
|
||||||
const style: CSSProperties = {
|
|
||||||
position: "absolute",
|
|
||||||
left: `${leftPx}px`,
|
|
||||||
top: `${topPx}px`,
|
|
||||||
width: `${widthPx}px`,
|
|
||||||
height: `${heightPx}px`,
|
|
||||||
zIndex: index,
|
|
||||||
pointerEvents: "none",
|
|
||||||
backdropFilter: `blur(${blurPx}px)`,
|
|
||||||
WebkitBackdropFilter: `blur(${blurPx}px)`,
|
|
||||||
...overlayBackground,
|
|
||||||
overflow: clipPath ? "hidden" : borderRadiusPx > 0 ? "hidden" : undefined,
|
|
||||||
borderRadius: borderRadiusPx > 0 ? `${borderRadiusPx}px` : undefined,
|
|
||||||
clipPath,
|
|
||||||
transform: rotation ? `rotate(${rotation}deg)` : undefined,
|
|
||||||
transformOrigin,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div key={`blur-backdrop-${obj.id}`} style={style} />;
|
|
||||||
})
|
|
||||||
;
|
|
||||||
}, [objects, scale, dragRevision, stageRef]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -186,10 +107,25 @@ const BlurBackdropLayer = ({ objects, stageWidth, stageHeight, scale, stageRef }
|
|||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{overlays}
|
{stackEntries.map((entry) => {
|
||||||
|
const livePosition = livePositions.get(entry.blurObject.id) ?? null;
|
||||||
|
const style = buildBlurOverlayStyle(entry.blurObject, scale, livePosition);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={entry.key} style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
|
||||||
|
<div style={style} />
|
||||||
|
<OcclusionObjectsStage
|
||||||
|
objects={entry.occlusionObjects}
|
||||||
|
allObjects={objects}
|
||||||
|
stageWidth={stageWidth}
|
||||||
|
stageHeight={stageHeight}
|
||||||
|
scale={scale}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BlurBackdropLayer;
|
export default BlurBackdropLayer;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Stage, Layer, Group } from "react-konva";
|
||||||
|
import ObjectRenderer from "./ObjectRenderer";
|
||||||
|
import type { EditorObject } from "../../store/editorStore";
|
||||||
|
|
||||||
|
type OcclusionObjectsStageProps = {
|
||||||
|
objects: EditorObject[];
|
||||||
|
allObjects: EditorObject[];
|
||||||
|
stageWidth: number;
|
||||||
|
stageHeight: number;
|
||||||
|
scale: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Re-renders objects above a blur overlay so they stay crisp (display-only). */
|
||||||
|
const OcclusionObjectsStage = ({
|
||||||
|
objects,
|
||||||
|
allObjects,
|
||||||
|
stageWidth,
|
||||||
|
stageHeight,
|
||||||
|
scale,
|
||||||
|
}: OcclusionObjectsStageProps) => {
|
||||||
|
if (objects.length === 0) return null;
|
||||||
|
|
||||||
|
const renderObject = (obj: EditorObject) => (
|
||||||
|
<ObjectRenderer
|
||||||
|
key={`occlusion-${obj.id}`}
|
||||||
|
obj={obj}
|
||||||
|
isSelected={false}
|
||||||
|
transformerRef={{ current: null }}
|
||||||
|
layerRef={{ current: null }}
|
||||||
|
onSelect={() => {}}
|
||||||
|
onUpdate={() => {}}
|
||||||
|
draggable={false}
|
||||||
|
allObjects={allObjects}
|
||||||
|
stageWidth={stageWidth}
|
||||||
|
stageHeight={stageHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stage
|
||||||
|
width={stageWidth * scale}
|
||||||
|
height={stageHeight * scale}
|
||||||
|
scaleX={scale}
|
||||||
|
scaleY={scale}
|
||||||
|
listening={false}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer listening={false}>
|
||||||
|
{objects
|
||||||
|
.filter((obj) => obj.type === "group")
|
||||||
|
.map(renderObject)}
|
||||||
|
{objects
|
||||||
|
.filter((obj) => !obj.groupId && obj.type !== "group")
|
||||||
|
.map(renderObject)}
|
||||||
|
{objects
|
||||||
|
.filter((obj) => obj.groupId && obj.type !== "group")
|
||||||
|
.map((obj) => {
|
||||||
|
const groupObject = objects.find((o) => o.id === obj.groupId);
|
||||||
|
if (!groupObject) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group key={`occlusion-group-member-${obj.id}`} listening={false}>
|
||||||
|
{renderObject(obj)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OcclusionObjectsStage;
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
import type { EditorObject } from "../store/editorStore";
|
||||||
|
import { getColorWithOpacity } from "./colorOpacity";
|
||||||
|
import { toCssGradientAngle, type LinearGradient } from "./gradient";
|
||||||
|
|
||||||
|
const TRIANGLE_TOP_Y_PERCENT = 6.69873;
|
||||||
|
const TRIANGLE_BOTTOM_Y_PERCENT = 93.30127;
|
||||||
|
const BLUR_TINT_OPACITY_FACTOR = 0.25;
|
||||||
|
|
||||||
|
export type BlurOverlayGeometry = {
|
||||||
|
leftPx: number;
|
||||||
|
topPx: number;
|
||||||
|
widthPx: number;
|
||||||
|
heightPx: number;
|
||||||
|
borderRadiusPx: number;
|
||||||
|
clipPath?: string;
|
||||||
|
transformOrigin: CSSProperties["transformOrigin"];
|
||||||
|
rotation: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isBlurBackdropObject(obj: EditorObject): boolean {
|
||||||
|
if (obj.visible === false) return false;
|
||||||
|
if (obj.isMask) return false;
|
||||||
|
if (obj.type !== "rectangle") return false;
|
||||||
|
return (obj.blur ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logical stage coords → overlay pixel space (matches Konva getAbsolutePosition after stage scale). */
|
||||||
|
export function computeBlurOverlayGeometry(
|
||||||
|
obj: EditorObject,
|
||||||
|
scale: number,
|
||||||
|
livePosition?: { x: number; y: number } | null,
|
||||||
|
): BlurOverlayGeometry {
|
||||||
|
const rotation = obj.rotation ?? 0;
|
||||||
|
const shapeType = obj.shapeType ?? "square";
|
||||||
|
const anchorX = livePosition?.x ?? (obj.x ?? 0) * scale;
|
||||||
|
const anchorY = livePosition?.y ?? (obj.y ?? 0) * scale;
|
||||||
|
|
||||||
|
if (shapeType === "circle") {
|
||||||
|
const sizePx = (obj.width ?? 100) * scale;
|
||||||
|
const radiusPx = sizePx / 2;
|
||||||
|
return {
|
||||||
|
leftPx: anchorX - radiusPx,
|
||||||
|
topPx: anchorY - radiusPx,
|
||||||
|
widthPx: sizePx,
|
||||||
|
heightPx: sizePx,
|
||||||
|
borderRadiusPx: radiusPx,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shapeType === "triangle") {
|
||||||
|
const side = Math.min(obj.width ?? 100, obj.height ?? 100);
|
||||||
|
const radiusPx = (side / 2) * scale;
|
||||||
|
return {
|
||||||
|
leftPx: anchorX - radiusPx,
|
||||||
|
topPx: anchorY - radiusPx,
|
||||||
|
widthPx: side * scale,
|
||||||
|
heightPx: side * scale,
|
||||||
|
borderRadiusPx: 0,
|
||||||
|
clipPath: `polygon(50% ${TRIANGLE_TOP_Y_PERCENT}%, 0% ${TRIANGLE_BOTTOM_Y_PERCENT}%, 100% ${TRIANGLE_BOTTOM_Y_PERCENT}%)`,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shapeType === "abstract") {
|
||||||
|
const rawW = obj.width ?? 100;
|
||||||
|
const rawH = obj.height ?? 100;
|
||||||
|
const baseRadius = (Math.min(rawW, rawH) / 2) * scale;
|
||||||
|
const radius = baseRadius * 0.85;
|
||||||
|
return {
|
||||||
|
leftPx: anchorX - radius,
|
||||||
|
topPx: anchorY - radius,
|
||||||
|
widthPx: radius * 2,
|
||||||
|
heightPx: radius * 2,
|
||||||
|
borderRadiusPx: 0,
|
||||||
|
clipPath:
|
||||||
|
"polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)",
|
||||||
|
transformOrigin: "center center",
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
leftPx: anchorX,
|
||||||
|
topPx: anchorY,
|
||||||
|
widthPx: (obj.width ?? 100) * scale,
|
||||||
|
heightPx: (obj.height ?? 100) * scale,
|
||||||
|
borderRadiusPx: Math.max(0, obj.borderRadius ?? 0) * scale,
|
||||||
|
transformOrigin: "0% 0%",
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBlurOverlayStyle(
|
||||||
|
obj: EditorObject,
|
||||||
|
scale: number,
|
||||||
|
livePosition?: { x: number; y: number } | null,
|
||||||
|
): CSSProperties {
|
||||||
|
const blur = obj.blur ?? 0;
|
||||||
|
const blurPx = blur * scale;
|
||||||
|
const geometry = computeBlurOverlayGeometry(obj, scale, livePosition);
|
||||||
|
const objOpacity = obj.opacity ?? 100;
|
||||||
|
const tintOpacity = Math.max(0, Math.min(100, objOpacity * BLUR_TINT_OPACITY_FACTOR));
|
||||||
|
const defaultFill = "#3b82f6";
|
||||||
|
|
||||||
|
const overlayBackground: CSSProperties =
|
||||||
|
obj.fillType === "gradient" && obj.gradient
|
||||||
|
? (() => {
|
||||||
|
const g = obj.gradient as LinearGradient;
|
||||||
|
const angle = toCssGradientAngle(g.angle);
|
||||||
|
const from = getColorWithOpacity(g.from, tintOpacity);
|
||||||
|
const to = getColorWithOpacity(g.to, tintOpacity);
|
||||||
|
return {
|
||||||
|
backgroundImage: `linear-gradient(${angle}deg, ${from} 0%, ${to} 100%)`,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: {
|
||||||
|
backgroundColor: getColorWithOpacity(obj.fill ?? defaultFill, tintOpacity),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: "absolute",
|
||||||
|
left: `${geometry.leftPx}px`,
|
||||||
|
top: `${geometry.topPx}px`,
|
||||||
|
width: `${geometry.widthPx}px`,
|
||||||
|
height: `${geometry.heightPx}px`,
|
||||||
|
pointerEvents: "none",
|
||||||
|
backdropFilter: `blur(${blurPx}px)`,
|
||||||
|
WebkitBackdropFilter: `blur(${blurPx}px)`,
|
||||||
|
...overlayBackground,
|
||||||
|
overflow:
|
||||||
|
geometry.clipPath
|
||||||
|
? "hidden"
|
||||||
|
: geometry.borderRadiusPx > 0
|
||||||
|
? "hidden"
|
||||||
|
: undefined,
|
||||||
|
borderRadius: geometry.borderRadiusPx > 0 ? `${geometry.borderRadiusPx}px` : undefined,
|
||||||
|
clipPath: geometry.clipPath,
|
||||||
|
transform: geometry.rotation ? `rotate(${geometry.rotation}deg)` : undefined,
|
||||||
|
transformOrigin: geometry.transformOrigin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlurStackEntry = {
|
||||||
|
key: string;
|
||||||
|
blurObject: EditorObject;
|
||||||
|
/** Objects rendered above this blur so they stay crisp. */
|
||||||
|
occlusionObjects: EditorObject[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildBlurStackEntries(objects: EditorObject[]): BlurStackEntry[] {
|
||||||
|
const entries: BlurStackEntry[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < objects.length; i++) {
|
||||||
|
const obj = objects[i];
|
||||||
|
if (!isBlurBackdropObject(obj)) continue;
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
key: obj.id,
|
||||||
|
blurObject: obj,
|
||||||
|
occlusionObjects: objects.slice(i + 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user