Skip to content

Commit c477282

Browse files
committed
Add background-blend-mode
1 parent 82fe0b5 commit c477282

8 files changed

Lines changed: 140 additions & 49 deletions

File tree

packages/backend/src/code.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ const memoizedVariableToColorName = async (
4444
variableId: string,
4545
): Promise<string> => {
4646
if (!variableCache.has(variableId)) {
47-
const colorName = await variableToColorName(variableId);
47+
const colorName = (await variableToColorName(variableId)).replaceAll(
48+
",",
49+
"",
50+
);
4851
variableCache.set(variableId, colorName);
52+
return colorName;
4953
}
5054
return variableCache.get(variableId)!;
5155
};
@@ -255,9 +259,18 @@ const processNodePair = async (
255259
const mutableSegment = Object.assign({}, segment);
256260

257261
if (settings.useColorVariables && segment.fills) {
258-
mutableSegment.fills = segment.fills.map((d) => ({ ...d }));
259-
await Promise.all(
260-
mutableSegment.fills.map((fill) => processColorVariables(fill)),
262+
mutableSegment.fills = await Promise.all(
263+
segment.fills.map(async (d) => {
264+
if (
265+
d.blendMode !== "PASS_THROUGH" &&
266+
d.blendMode !== "NORMAL"
267+
) {
268+
addWarning("BlendMode is not supported in Text colors");
269+
}
270+
const fill = { ...d };
271+
await processColorVariables(fill);
272+
return fill;
273+
}),
261274
);
262275
}
263276

@@ -298,8 +311,12 @@ const processNodePair = async (
298311
if ("width" in figmaNode) {
299312
jsonNode.width = figmaNode.width;
300313
jsonNode.height = figmaNode.height;
301-
jsonNode.x = figmaNode.x;
302-
jsonNode.y = figmaNode.y;
314+
// jsonNode.x = figmaNode.x;
315+
// jsonNode.y = figmaNode.y;
316+
}
317+
318+
if ("rotation" in jsonNode) {
319+
jsonNode.rotation = jsonNode.rotation * (180 / Math.PI);
303320
}
304321

305322
if ("individualStrokeWeights" in jsonNode) {

packages/backend/src/common/convertFontWeight.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { FontWeightNumber } from "types";
33
// Convert generic named weights to numbers, which is the way tailwind understands
44
export const convertFontWeight = (weight: string): FontWeightNumber | null => {
55
// change extra-light to extralight
6-
weight = weight.replace(" ", "").replace("-", "").toLowerCase();
6+
weight = weight.replaceAll(" ", "").replaceAll("-", "").toLowerCase();
77
switch (weight) {
88
case "thin":
99
return "100";

packages/backend/src/html/builderImpl/htmlColor.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,52 @@ export const htmlDiamondGradient = (fill: GradientPaint): string => {
243243
)
244244
.join(", ");
245245
};
246+
247+
export const buildBackgroundValues = (
248+
paintArray: ReadonlyArray<Paint> | PluginAPI["mixed"],
249+
settings: HTMLSettings,
250+
): string => {
251+
if (paintArray === figma.mixed) {
252+
return "";
253+
}
254+
255+
// If only one fill, just use plain color/gradient
256+
if (paintArray.length === 1) {
257+
const paint = paintArray[0];
258+
if (paint.type === "SOLID") {
259+
return htmlColorFromFills(paintArray, settings);
260+
} else if (
261+
paint.type === "GRADIENT_LINEAR" ||
262+
paint.type === "GRADIENT_RADIAL" ||
263+
paint.type === "GRADIENT_ANGULAR" ||
264+
paint.type === "GRADIENT_DIAMOND"
265+
) {
266+
return htmlGradientFromFills(paint);
267+
}
268+
return "";
269+
}
270+
271+
// Reverse the array to match CSS layering (first is top-most in CSS)
272+
const styles = [...paintArray].reverse().map((paint, index) => {
273+
if (paint.type === "SOLID") {
274+
// For multiple fills, always convert solid colors to linear gradients
275+
// to ensure proper layering in CSS backgrounds
276+
const color = htmlColorFromFills([paint], settings);
277+
if (index === 0) {
278+
return `linear-gradient(0deg, ${color} 0%, ${color} 100%)`;
279+
}
280+
281+
return color;
282+
} else if (
283+
paint.type === "GRADIENT_LINEAR" ||
284+
paint.type === "GRADIENT_RADIAL" ||
285+
paint.type === "GRADIENT_ANGULAR" ||
286+
paint.type === "GRADIENT_DIAMOND"
287+
) {
288+
return htmlGradientFromFills(paint);
289+
}
290+
return ""; // Handle other paint types safely
291+
});
292+
293+
return styles.filter((value) => value !== "").join(", ");
294+
};

packages/backend/src/html/builderImpl/htmlSize.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export const htmlSizePartial = (
1616
}
1717

1818
const size = nodeSize(node);
19-
console.log("size", size);
2019
const nodeParent =
2120
(node.parent && optimizeLayout && "inferredAutoLayout" in node.parent
2221
? node.parent.inferredAutoLayout

packages/backend/src/html/htmlDefaultBuilder.ts

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
htmlBlendMode,
88
} from "./builderImpl/htmlBlend";
99
import {
10+
buildBackgroundValues,
1011
htmlColorFromFills,
11-
htmlGradientFromFills,
1212
} from "./builderImpl/htmlColor";
1313
import { htmlPadding } from "./builderImpl/htmlPadding";
1414
import { htmlSizePartial } from "./builderImpl/htmlSize";
@@ -31,7 +31,6 @@ import { HTMLSettings } from "types";
3131
import {
3232
cssCollection,
3333
generateUniqueClassName,
34-
getSvelteClassName,
3534
stylesToCSS,
3635
} from "./htmlMain";
3736

@@ -122,12 +121,6 @@ export class HtmlDefaultBuilder {
122121
this.autoLayoutPadding();
123122
this.position();
124123
this.blend();
125-
126-
// Add z-index if we have a custom value from the itemReverseZIndex handling
127-
if ((this.node as any).customZIndex !== undefined) {
128-
this.addStyles(`z-index: ${(this.node as any).customZIndex}`);
129-
}
130-
131124
return this;
132125
}
133126

@@ -229,8 +222,11 @@ export class HtmlDefaultBuilder {
229222

230223
position(): this {
231224
const { node, optimizeLayout, isJSX } = this;
232-
if (commonIsAbsolutePosition(node, optimizeLayout)) {
225+
const isAbsolutePosition = commonIsAbsolutePosition(node, optimizeLayout);
226+
console.log("isAbsolutePosition", isAbsolutePosition, "for node", node);
227+
if (isAbsolutePosition) {
233228
const { x, y } = getCommonPositionValue(node);
229+
console.log("x, y, are", x, y);
234230

235231
this.addStyles(
236232
formatWithJSX("left", isJSX, x),
@@ -267,44 +263,39 @@ export class HtmlDefaultBuilder {
267263
return this;
268264
}
269265

270-
const backgroundValues = this.buildBackgroundValues(paintArray);
266+
const backgroundValues = buildBackgroundValues(paintArray, this.settings);
271267
if (backgroundValues) {
272268
this.addStyles(formatWithJSX("background", this.isJSX, backgroundValues));
269+
270+
// Add blend mode property if multiple fills exist with different blend modes
271+
if (paintArray !== figma.mixed) {
272+
const blendModes = this.buildBackgroundBlendModes(paintArray);
273+
if (blendModes) {
274+
this.addStyles(
275+
formatWithJSX("background-blend-mode", this.isJSX, blendModes),
276+
);
277+
}
278+
}
273279
}
274280

275281
return this;
276282
}
277283

278-
buildBackgroundValues(
279-
paintArray: ReadonlyArray<Paint> | PluginAPI["mixed"],
280-
): string {
281-
if (paintArray === figma.mixed) {
284+
buildBackgroundBlendModes(paintArray: ReadonlyArray<Paint>): string {
285+
if (paintArray.length === 0) {
282286
return "";
283287
}
284288

285-
// If one fill and it's a solid, return the solid RGB color
286-
if (paintArray.length === 1 && paintArray[0].type === "SOLID") {
287-
return htmlColorFromFills(paintArray, this.settings);
288-
}
289-
290-
// If multiple fills, deal with gradients and convert solid colors to a "dumb" linear-gradient
291-
const styles = paintArray.map((paint) => {
292-
if (paint.type === "SOLID") {
293-
const color = htmlColorFromFills([paint], this.settings);
294-
return `linear-gradient(0deg, ${color} 0%, ${color} 100%)`;
295-
} else if (
296-
paint.type === "GRADIENT_LINEAR" ||
297-
paint.type === "GRADIENT_RADIAL" ||
298-
paint.type === "GRADIENT_ANGULAR" ||
299-
paint.type === "GRADIENT_DIAMOND"
300-
) {
301-
return htmlGradientFromFills(paint);
289+
// Reverse the array to match the background order
290+
const blendModes = [...paintArray].reverse().map((paint) => {
291+
if (paint.blendMode === "PASS_THROUGH") {
292+
return "normal";
302293
}
303294

304-
return ""; // Handle other paint types safely
295+
return paint.blendMode?.toLowerCase();
305296
});
306297

307-
return styles.filter((value) => value !== "").join(", ");
298+
return blendModes.join(", ");
308299
}
309300

310301
shadow(): this {
@@ -374,7 +365,7 @@ export class HtmlDefaultBuilder {
374365
formatWithJSX(
375366
"filter",
376367
this.isJSX,
377-
`blur(${numberToFixedString(blur.radius)}px)`,
368+
`blur(${numberToFixedString(blur.radius / 2)}px)`,
378369
),
379370
);
380371
}
@@ -387,7 +378,7 @@ export class HtmlDefaultBuilder {
387378
formatWithJSX(
388379
"backdrop-filter",
389380
this.isJSX,
390-
`blur(${numberToFixedString(backgroundBlur.radius)}px)`,
381+
`blur(${numberToFixedString(backgroundBlur.radius / 2)}px)`,
391382
),
392383
);
393384
}

packages/backend/src/tailwind/builderImpl/tailwindBlend.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,30 @@ export const tailwindBlendMode = (node: MinimalBlendMixin): string => {
5555
return "";
5656
};
5757

58+
/**
59+
* Convert a Figma background blend mode to a Tailwind bg-blend-* class
60+
*
61+
* @param paintArray The array of paint fills that may have blend modes
62+
* @returns Tailwind background blend mode class if applicable
63+
*/
64+
export const tailwindBackgroundBlendMode = (
65+
paintArray: ReadonlyArray<Paint>,
66+
): string => {
67+
if (paintArray.length === 0) {
68+
return "";
69+
}
70+
71+
// Get the top fill's blend mode (in Figma, the last item is the top one)
72+
const topFill = paintArray[paintArray.length - 1];
73+
if (topFill.blendMode === "NORMAL" || topFill.blendMode === "PASS_THROUGH") {
74+
return "";
75+
}
76+
77+
const blendMode =
78+
topFill.blendMode?.toLowerCase()?.replaceAll("_", "-") || "normal";
79+
return `bg-blend-${blendMode}`;
80+
};
81+
5882
/**
5983
* https://tailwindcss.com/docs/visibility/
6084
* example: invisible

packages/backend/src/tailwind/tailwindDefaultBuilder.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
tailwindRotation,
99
tailwindOpacity,
1010
tailwindBlendMode,
11+
tailwindBackgroundBlendMode,
1112
} from "./builderImpl/tailwindBlend";
1213
import {
1314
tailwindBorderWidth,
@@ -167,6 +168,14 @@ export class TailwindDefaultBuilder {
167168
let gradient = "";
168169
if (kind === "bg") {
169170
gradient = tailwindGradientFromFills(paint);
171+
172+
// Add background blend mode class if applicable
173+
if (paint !== figma.mixed) {
174+
const blendModeClass = tailwindBackgroundBlendMode(paint);
175+
if (blendModeClass) {
176+
this.addAttributes(blendModeClass);
177+
}
178+
}
170179
}
171180
if (gradient) {
172181
this.addAttributes(gradient);
@@ -234,9 +243,11 @@ export class TailwindDefaultBuilder {
234243
blur() {
235244
const { node } = this;
236245
if ("effects" in node && node.effects.length > 0) {
237-
const blur = node.effects.find((e) => e.type === "LAYER_BLUR");
246+
const blur = node.effects.find(
247+
(e) => e.type === "LAYER_BLUR" && e.visible,
248+
);
238249
if (blur) {
239-
const blurValue = pxToBlur(blur.radius);
250+
const blurValue = pxToBlur(blur.radius / 2);
240251
if (blurValue) {
241252
this.addAttributes(
242253
blurValue === "blur" ? "blur" : `blur-${blurValue}`,
@@ -245,10 +256,10 @@ export class TailwindDefaultBuilder {
245256
}
246257

247258
const backgroundBlur = node.effects.find(
248-
(e) => e.type === "BACKGROUND_BLUR",
259+
(e) => e.type === "BACKGROUND_BLUR" && e.visible,
249260
);
250261
if (backgroundBlur) {
251-
const backgroundBlurValue = pxToBlur(backgroundBlur.radius);
262+
const backgroundBlurValue = pxToBlur(backgroundBlur.radius / 2);
252263
if (backgroundBlurValue) {
253264
this.addAttributes(
254265
`backdrop-blur${

packages/backend/src/tailwind/tailwindTextBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder {
137137
}
138138

139139
const value = node.fontName.style
140-
.replace("italic", "")
141-
.replace(" ", "")
140+
.replaceAll("italic", "")
141+
.replaceAll(" ", "")
142142
.toLowerCase();
143143

144144
this.addAttributes(`font-${value}`);

0 commit comments

Comments
 (0)