Add background blur to text shapes

This commit is contained in:
Elena Torro 2026-07-16 07:47:21 +02:00
parent bdc078d5ea
commit 001dba1bef
6 changed files with 206 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,146 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"tokens/numeric-input",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u8b485740-3f39-8080-8008-400e7784f55a",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "New File 3",
"~:revn": 54,
"~:modified-at": "~m1784183383968",
"~:vern": 0,
"~:id": "~u814272d9-d3f8-812d-8008-54c11cbba219",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node",
"0017-fix-layout-flex-dir",
"0018-remove-unneeded-objects-from-components",
"0019-fix-missing-swap-slots",
"0020-sync-component-id-with-near-main",
"0021-fix-shape-svg-attrs",
"0022-normalize-component-root-and-resync",
"0023-repair-token-themes-with-inexistent-sets",
"0024b-fix-stroke-cap-placement"
]
},
"~:version": 67,
"~:project-id": "~u8b485740-3f39-8080-8008-400e7786d1d0",
"~:created-at": "~m1784121921262",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u814272d9-d3f8-812d-8008-54c11cbba21a"
],
"~:pages-index": {
"~u814272d9-d3f8-812d-8008-54c11cbba21a": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u9f14b915-410b-8062-8008-55a1fbbd5fa4\",\"~u9f14b915-410b-8062-8008-559878019a9e\"]]]",
"~u9f14b915-410b-8062-8008-559878019a9e": "[\"~#shape\",[\"^ \",\"~:y\",-1863.999482267452,\"~:background-blur\",[\"^ \",\"~:id\",\"~u9f14b915-410b-8062-8008-5598c30393d7\",\"~:type\",\"^1\",\"~:value\",8,\"~:hidden\",false],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:auto-width\",\"~:content\",[\"^ \",\"^3\",\"root\",\"~:key\",\"22sle9m74ka\",\"~:children\",[[\"^ \",\"^3\",\"paragraph-set\",\"^=\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^=\",[[\"^ \",\"^?\",\"normal\",\"~:text-transform\",\"none\",\"~:font-id\",\"sourcesanspro\",\"^<\",\"1ixh2em79as\",\"~:font-size\",\"200\",\"~:font-weight\",\"900\",\"~:font-variant-id\",\"black\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.12]],\"~:font-family\",\"sourcesanspro\",\"~:text\",\"blur\"]],\"^@\",\"none\",\"~:text-align\",\"left\",\"^A\",\"sourcesanspro\",\"^<\",\"1x2xz4p3kcs\",\"^B\",\"200\",\"^C\",\"900\",\"~:text-direction\",\"ltr\",\"^3\",\"paragraph\",\"^D\",\"black\",\"^E\",\"none\",\"^F\",\"0\",\"^G\",[[\"^ \",\"^H\",\"#ffffff\",\"^I\",0.12]],\"^J\",\"sourcesanspro\"]]]],\"~:vertical-align\",\"top\"],\"~:hide-in-viewer\",false,\"~:name\",\"blur\",\"~:width\",375.0000213699236,\"^3\",\"^K\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",95.49998073772147,\"~:y\",-1863.999482267452]],[\"^S\",[\"^ \",\"~:x\",470.50000210764506,\"~:y\",-1863.999482267452]],[\"^S\",[\"^ \",\"~:x\",470.50000210764506,\"~:y\",-1623.9994721448552]],[\"^S\",[\"^ \",\"~:x\",95.49998073772147,\"~:y\",-1623.9994721448552]]],\"~:transform-inverse\",[\"^7\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"^2\",\"~u9f14b915-410b-8062-8008-559878019a9e\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:position-data\",[[\"^ \",\"~:y\",-1602.939453125,\"^>\",\"1.2\",\"^?\",\"normal\",\"~:typography-ref-id\",null,\"^@\",\"none\",\"^L\",\"left\",\"^A\",\"sourcesanspro\",\"^B\",\"200px\",\"^C\",\"900\",\"~:typography-ref-file\",null,\"^M\",\"ltr\",\"^Q\",374.6099853515625,\"^D\",\"regular\",\"^E\",\"none\",\"^F\",\"0px\",\"~:x\",95.4999771118164,\"^G\",[[\"^ \",\"^H\",\"#ffffff\",\"^I\",0.12]],\"~:direction\",\"ltr\",\"^J\",\"sourcesanspro\",\"~:height\",282.1201171875,\"^K\",\"blur\"]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",95.49998073772146,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",95.49998073772146,\"~:y\",-1863.999482267452,\"^Q\",375.0000213699236,\"^Z\",240.0000101225969,\"~:x1\",95.49998073772146,\"~:y1\",-1863.999482267452,\"~:x2\",470.50000210764506,\"~:y2\",-1623.9994721448552]],\"~:flip-x\",null,\"^Z\",240.0000101225969,\"~:flip-y\",null]]",
"~u9f14b915-410b-8062-8008-55a1fbbd5fa4": "[\"~#shape\",[\"^ \",\"~:y\",-2043.9994670845845,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"background_1\",\"~:width\",599.9999796086561,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",-16.999998381644758,\"~:y\",-2043.9994670845845]],[\"^9\",[\"^ \",\"~:x\",582.9999812270113,\"~:y\",-2043.9994670845845]],[\"^9\",[\"^ \",\"~:x\",582.9999812270113,\"~:y\",-1443.9994873277228]],[\"^9\",[\"^ \",\"~:x\",-16.999998381644758,\"~:y\",-1443.9994873277228]]],\"~:r2\",0,\"~:proportion-lock\",true,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u9f14b915-410b-8062-8008-55a1fbbd5fa4\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",-16.999998381644787,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",-16.999998381644787,\"~:y\",-2043.9994670845845,\"^5\",599.9999796086561,\"~:height\",599.9999797568616,\"~:x1\",-16.999998381644787,\"~:y1\",-2043.9994670845845,\"~:x2\",582.9999812270113,\"~:y2\",-1443.9994873277228]],\"~:fills\",[[\"^ \",\"~:fill-opacity\",1,\"~:fill-image\",[\"^ \",\"^5\",1181,\"^G\",1181,\"~:mtype\",\"image/png\",\"^?\",\"~u814272d9-d3f8-812d-8008-55a1fb78211b\",\"~:keep-aspect-ratio\",true]]],\"~:flip-x\",null,\"^G\",599.9999797568616,\"~:flip-y\",null]]"
}
},
"~:id": "~u814272d9-d3f8-812d-8008-54c11cbba21a",
"~:name": "Page 1"
}
},
"~:id": "~u814272d9-d3f8-812d-8008-54c11cbba219",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@ -609,4 +609,22 @@ test("Renders a file with group with strokes and not 100% opacities", async ({
maxDiffPixelRatio: 0,
threshold: 0.01,
});
});
});
test("Renders background blur on text shapes", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockFileMediaAsset(
"814272d9-d3f8-812d-8008-55a1fb78211b",
"render-wasm/assets/squares-background.png",
);
await workspace.mockGetFile("render-wasm/get-file-text-background-blur.json");
await workspace.goToWorkspace({
id: "814272d9-d3f8-812d-8008-54c11cbba219",
pageId: "814272d9-d3f8-812d-8008-54c11cbba21a",
});
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

View File

@ -660,8 +660,7 @@ impl RenderState {
if self.options.is_fast_mode() {
return;
}
if matches!(shape.shape_type, Type::Text(_)) || matches!(shape.shape_type, Type::SVGRaw(_))
{
if matches!(shape.shape_type, Type::SVGRaw(_)) {
return;
}
let blur = match shape.visible_background_blur() {
@ -707,6 +706,40 @@ impl RenderState {
canvas.translate(translation);
canvas.concat(&matrix);
if matches!(shape.shape_type, Type::Text(_)) {
canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true);
// Blur in device space
canvas.reset_matrix();
// Save the layer rect to the shape's selection rect
// so the blur is only applied inside the glyphs.
let layer_rec = skia::canvas::SaveLayerRec::default()
.backdrop(&blur_filter)
.backdrop_tile_mode(skia::TileMode::Clamp);
canvas.save_layer(&layer_rec);
// Reset
canvas.scale((scale, scale));
canvas.translate(translation);
canvas.concat(&matrix);
// Keep the blurred backdrop only where the glyphs are
let mut mask_paint = skia::Paint::default();
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
let mask_layer_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
canvas.save_layer(&mask_layer_rec);
text::paint_text_mask(canvas, shape);
// Restore the mask layer, then the blur layer,
// then the shape transform
canvas.restore();
canvas.restore();
canvas.restore();
return;
}
// Clip to shape's path based on shape type
match &shape.shape_type {
Type::Rect(data) if data.corners.is_some() => {

View File

@ -396,6 +396,12 @@ fn paint_text(
paint_text_with_emoji_overlay(canvas, shape, paragraph_builder_groups, false);
}
/// Alpha mask for background blur coverage
pub fn paint_text_mask(canvas: &Canvas, shape: &Shape) {
let mut mask_builders = shape.get_text_content().paragraph_builder_group_opaque();
paint_text(canvas, shape, &mut mask_builders);
}
fn paint_text_with_emoji_overlay(
canvas: &Canvas,
shape: &Shape,