Commit Graph
307 Commits
Author SHA1 Message Date
Belén Albeza 0581c35452 🐛 Fix drop shadows in masks (#11754)
* 🐛 Fix shadows on a masked group in the WASM renderer

A masked group renders in two passes: its content, then the mask shape
composited with DstIn so everything outside the mask silhouette is
erased. Both happen inside one save_layer, and the group's drop shadow
was composited into that same layer before the mask pass — so the mask
erased it. A drop shadow lives mostly outside the silhouette, so it
disappeared entirely.

Inner shadows never drew at all: render_fill_inner_shadows needs fill
geometry to paint into, and a group has none.

Both now ride on an image filter set on the masked-group layer, which
Skia evaluates after the mask is composited, so the shadow comes from
the real masked pixels rather than the group's own, empty geometry.
The effects compose in the order the SVG renderer uses for a group:
drop shadows, then the source, then inner shadows, with the layer blur
over all of it.

That layer is opened on a canvas carrying no transform, so the filter
is built in device units. Shadow::scale_to_device does that rather
than scale_content, because radius_to_sigma is affine: scaling the
radius applies its constant term once at device scale, while a filter
built in document space has the term scaled by the canvas matrix. The
two would blur differently by 0.5 · (scale - 1) sigma, visible as a
masked group's own shadow being narrower than the same shadow on its
parent. The masked-group layer blur had the same flaw.

Three paths are suppressed for masked groups so nothing is drawn twice:
the silhouette composite, the nested_shadows inheritance that would
reach text descendants, and the fill inner-shadow pass.

Every save and restore around that layer is keyed on the shape alone.
Enter and exit run on different walker passes, and a pan or zoom in
between changes fast mode, so deriving them from render state could
leave the canvas clip stack unbalanced.

Refs #11697

AI-assisted-by: claude-opus-5

* 🐛 Fix a container's drop shadow over a masked group in WASM

A container builds its drop shadow by drawing each descendant as a
black silhouette and blurring the result. The walk descends only
through children that can be flattened, and a masked group never can,
so it stopped there and asked the group to draw its own geometry. A
group has none, so nothing was drawn and the shadow layer stayed
blank: no shadow at all for a group, board or frame holding a masked
group. This is what the file attached to the issue reproduces.

render_drop_black_shadow now draws the masked silhouette for such a
group — content children flat black, DstIn the mask, and only then the
offset, blur and spread. Masking after the blur would trim the shadow
along the wrong edge.

The walk recurses, so it narrows the clip the way the main walker
does: content a clipping container hides must not widen the shadow.
The clip rule now lives in one place, shared with the walker, and a
test pins the two against each other. The shadow layer is sized to the
silhouette plus the shadow's reach rather than falling back to the
clip, so a wide blur is not cut at the tile edge.

Spread keeps the renderer's existing behaviour: the silhouette goes
through the same get_drop_shadow_filter every other shadow uses, so a
masked group gains no ordering of its own.

Closes #11697

AI-assisted-by: claude-opus-5
2026-09-18 13:04:11 +02:00
Elena Torró cc9c60507d 🐛 Do not apply stroke style to caps (#11744) 2026-09-17 09:39:39 +02:00
Alejandro Alonso 20547f658e 🐛 Preserve tile coverage when packing atlas slots (#11749)
Packed atlas compose inset Linear samples after blitting the full
drawable into the slot, which dropped edge texels and opened
multi-pixel gaps on text that crossed tile seams (#11696), especially
under HiDPI packing. Write the drawable into the inset content rect
and clamp-pad the 1px frame so Linear compose keeps coverage without
bleeding into the next cell.

Closes #11696
2026-09-16 16:40:33 +02:00
Alejandro Alonso fcae641c7b 🐛 Skip content clip on WASM SVG drop silhouettes (#11655)
Clipped frames (show-content=false) were applying clipPath to the drop
silhouette. Spread/offset fills were outset correctly but then truncated
to the true selrect, so spread rings vanished on export. Match the GPU:
paint drop silhouettes outside the content clip; keep clipping on the
real content pass only.

Closes #11653
2026-09-16 16:08:17 +02:00
Alejandro Alonso b8af89c1b8 Export text strokes to WASM SVG (#11578)
SkSVGDevice drops save_layer composition for text strokes, so emit
center strokes (with <g opacity> when alpha), inner strokes via glyph
clipPath, and outer strokes via inverse-glyph luminance mask.

Closes #11386
2026-09-16 15:54:07 +02:00
ShreyashandShreyash Agare 76e9289fcf 🐛 Fix stroke to path dropping caps and markers (#11614)
* 🐛 Fix stroke to path dropping caps and markers

Caps and markers (round ends, arrows, square/diamond/circle markers) are
painted separately by `handle_stroke_caps` after the stroke itself, so
`stroke_to_path` — which only outlines the shape path — dropped them.
Converting a stroke to a path lost every end decoration except the
Round/Round and Square/Square pairs Skia draws natively, and even those
were missing because the outline paint kept the default butt cap.

Cap geometry now lives in `shapes/stroke_paths.rs` as plain path
builders, shared by the canvas renderer and by `stroke_to_path`, which
unions the caps into the outline for open paths and honors
`to_skia_linecap`. The SVG export no longer overlays the caps a second
time on top of the expanded outline, which would double the alpha of
translucent strokes; the clip silhouette for image strokes gets them
from the outline too.

Co-authored-by: Shreyash Agare <agareshreyash26@gmail.com>
2026-09-16 12:46:00 +02:00
Alejandro Alonso 3adf9ade14 Export drop and inner shadows to WASM SVG (#11593)
SkSVGDevice drops save_layer image-filters, so re-emit visible drop and
inner shadows (plus optional layer blur) as one native SVG filter chain
matching classic order: flood, drops, SourceGraphic, inners, blur.

Closes #11379
2026-09-11 13:52:57 +02:00
Alejandro Alonso ffad71bdc5 🐛 Hard-clip tile atlas blit to avoid seam hairlines (#11639)
AA clip on the Current→atlas blit softens shared tile edges so
the canvas background shows through as 1px lines at the 512px
grid when tiles are composed with SrcOver.

Closes #11638
2026-09-11 09:22:36 +02:00
Elena Torró 0913545b41 🐛 Fix letter spacing and position when flattening text to path (#11555)
* 🐛 Fix letter spacing and position when flattening text to path

* ♻️ Simplify text to path conversion and add flatten tests
2026-09-11 08:20:22 +02:00
Alejandro Alonso aa78ca0be8 🐛 Keep text image fills aligned during drag (#11610)
Cached Skia paragraphs bake absolute image/gradient shaders at layout
time. On move, clones reuse those paragraphs while painting at the new
selrect, so glyphs move and the fill stays put. Record the paint origin
when layout is built and translate the canvas when painting from cache
so shaders track the text. Also sync bounds before update_layout so
fills bake against the current container.
2026-09-10 16:55:45 +02:00
Alejandro Alonso dbe5941a23 Export layer blur to WASM SVG (#11580)
SkSVGDevice drops paint image-filters, so re-emit visible layer blur as a
native feGaussianBlur filter on the composite <g>. Match canvas sigma via
radius_to_sigma(value * scale), and skip Skia blur filters on the SVG
VectorRenderer path so shapes do not vanish.

Closes #11380
2026-09-10 14:25:47 +02:00
Alejandro Alonso dc12f1db91 Export image-filled strokes to WASM SVG as linked images (#11559)
Closes #11384

Skia's SVG backend drops save_layer+SrcIn, so image strokes are re-emitted
as a linked <image> clipped to an opaque stroke silhouette (filled outline,
clip-rule evenodd). Open-path caps join the silhouette and grow the image
dest by cap_bounds_margin so markers stay textured.
2026-09-10 13:29:14 +02:00
Elena Torró 9b24907992 🐛 Fix render text-decoration on spans and update regression tests (#11583) 2026-09-10 09:42:09 +02:00
Elena Torró 61caefbcf3 🔧 Support independent image bounds on wasm export (#11590) 2026-09-09 18:27:22 +02:00
Danny ShirelyandAndrey Antukh d45c6710b7 🎉 Implement independent image bounds resizing (#11430)
* 🎉 Implement independent image bounds resizing

Add canvas resize interaction mode that allows users to resize an
image object's bounding box independently from the underlying bitmap
content without scaling or distortion while holding the Mod key.

AI-assisted-by: gemini-2.5-pro

* ♻️ Address reviewer feedback from elenatorro

- Remove legacy cfh/image-shape? check in shape-has-image-fill?
- Guard bounds-resize with positive dimensions instead of clamping scalev to preserve flipping
- Remove :metadata from transform-attrs in modifiers.cljs
- Restore preserveAspectRatio logic based on keep-ar? in fills.cljs
- Compute source rect against destination rect for raster and SVG fills in WASM renderer

* 🔧 Fix clippy needless borrow warnings in wasm image fills

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-09 15:16:56 +02:00
Alejandro Alonso e67ebacef2 🐛 Export svg-raw shapes through WASM SVG draw_svg (#11567)
SVG export treated SVGRaw like a fill/stroke leaf and never called
draw_svg, so imported SVG content exported as an empty document.

Relates to #10546
2026-09-09 11:16:01 +02:00
Alejandro Alonso 2e5157e6aa Export image fills to WASM SVG as linked images (#11529)
Emit clipped `<image href>` for fills with a registered media URL instead
of Skia base64 embeds. Register URLs from the frontend and exporter via
store_image_url. Cover rect, path, frame, and text (glyph clip) cases in
SVG export tests.

Closes #11383
2026-09-08 17:30:53 +02:00
Alejandro Alonso 269aa36f82 Export solid and patterned strokes to WASM SVG (#11513)
*  Export solid inner and outer strokes to WASM SVG

SkSVGDevice drops save_layer+Clear used for path Outer strokes, so
solid Inner/Outer strokes on closed shapes are expanded to filled
outlines via stroke_to_path. Center and open paths stay on the shared
stroke path.

Closes #11381

*  Export dotted, dashed, and mixed strokes to WASM SVG

PathEffects do not survive SkSVGDevice, so dotted/dashed/mixed strokes
expand via stroke_to_path with the effect kept, producing filled
outline geometry for all alignments including Center.

Closes #11382

* 🐛 Fix WASM SVG stroke export edge cases

Keep expanded stroke outlines in local space under the leaf CTM so
rotated rects/paths are not double-transformed; overlay open-path caps
after dotted expansion; draw frame strokes outside the content clip;
and route per-side rect/frame strokes through the evenodd band path.
2026-09-07 13:50:58 +02:00
Alejandro Alonso c5897bc50a Paint plain text directly onto Current (#11355)
* ♻️ Share text layout paragraphs across modifier clones

Store Skia paragraphs in Rc so TextContentLayout::clone keeps the
cached layout for rotate/pan modifiers. Add layout.clear() and treat
needs_update as paragraphs-empty only.

*  Reuse cached Skia paragraphs when painting text

Add try_paint_from_layout_cache to paint from TextContent.layout when
versions match, skipping ParagraphBuilder rebuild and layout on each
frame. Wire into the layered text path for plain fills without strokes
or effects.

*  Paint plain text directly onto Current

Extend can_render_directly for stroke-free text and skip the empty
save_layer in draw_text when no stroke-group opacity is set. Plain text
paints into Current without the Fills/Strokes blit.
2026-09-02 16:17:06 +02:00
Alejandro Alonso b92112da54 🐛 Export multiple solid fills to WASM SVG (#11467)
Shapes and text with stacked solid fills were exported without fill
attributes because merge_fills builds a Skia shader that SkSVGDevice
does not serialize. Draw each fill layer separately for SVG export,
bottom to top, matching the classic exporter.

Closes #11466
2026-09-02 13:40:05 +02:00
Elena Torró 03e6f119e5 ♻️ Clean unnecessary methods (#11472)
* ♻️ Remove duplicated/unused set-children code

* ♻️ Remove unused methods
2026-09-02 12:58:52 +02:00
Alejandro Alonso d1a54134bb Add text and @font-face to WASM SVG export (#11442)
*  Add text and @font-face to WASM SVG export

Emit selectable <text> elements and inject @font-face rules that
reference the font URLs registered at load time. Text fill bypasses
save_layer wrappers that SkSVGDevice drops. Text strokes are deferred
to a follow-up PR.

* 🐛 Strip trailing commas from Skia SVG text positions

Skia's SVG backend appends a trailing comma to per-glyph x/y lists.
Firefox rejects the malformed attribute and drops glyph positioning,
making exported text vanish or mis-render.
2026-09-01 17:06:50 +02:00
Alejandro Alonso 89a17df403 Add basic WASM SVG export path (#10891)
Introduce a minimal vector SVG exporter via render-wasm: solid fills,
native opacity/blend wrappers, frame clip-content, FFI/frontend wiring,
insta snapshots, and a visual preview gallery. Defer save_layer
workarounds (shadows, blur, masks, special strokes) to follow-up PRs.
2026-08-31 16:06:38 +02:00
Alejandro Alonso 7419bc7007 🐛 Evict multi-scale tile cache on shape edits (#11337)
those textures across zoom for progressive previews, and invalidate
by old∪new document coverage so rotate/move edits do not leave
stale fragments on zoom-out.
2026-08-26 08:25:23 +02:00
Alejandro Alonso 5dab689a6e 🐛 Pack tile atlas and clamp HiDPI surfaces under GPU limits
HQ tiles are 512px and the atlas stays at 4096² (64 full-size
slots). Browser zoom plus a forced ?dpr= can need more visible
tiles than that, and a framebuffer larger than the GPU allows.

Pack interest tiles into smaller atlas cells, blit at 512 then
scale, and inset Linear samples so seams do not bleed. Clamp the
canvas backing store and DPR together, wrap Skia at the real
drawingBuffer size, and wait one frame after DPR changes so CSS
client size and overlays stay aligned.
2026-08-21 11:17:16 +02:00
Alejandro Alonso 689d506788 Render eligible frame drop shadows via direct geometry path
Add a direct container-geometry path for eligible frames: inline blur
when the kernel fits the tile margin, otherwise a cached filter-surface
pass reused across tiles via DropShadowFilterCache on both the direct
and slow render_shape paths.

Move frame shadow logic into shadows.rs. Fix nested/clipped frame
shadows by deferring parent clip to composite time, apply negative
spread via inset, and allow rotated/transformed frames on the direct
path. Skip descendant extrect walks for clipped frames when only
nested drop shadows matter, and skip child silhouettes when the
container fill already covers shadow descendants.
2026-08-21 11:17:16 +02:00
Elena Torró ed588d4500 Disable ReduceOpsTaskSplitting Skia flag (#11280) 2026-08-19 17:27:50 +02:00
Belén Albeza b6c4cb48d7 🐛 Fix not being able to select right or center-aligned text in v3 (#11258) 2026-08-18 18:13:47 +02:00
Alejandro Alonso cb57fd9dfa Skip save_layer for plain image fills (#11230)
Avoid an offscreen buffer per Fill::Image during tile walks: only use
save_layer when a shape image filter is present; axis-aligned rects and
frames without corner radii also skip the redundant container clip.
2026-08-13 12:12:43 +02:00
Alejandro Alonso 43b12bc4b9 Soft-drain GPU mid-walk on progressive Partials (#11127)
Release packs far more cheap Current draws (e.g. fills_none paths)
into one Partial than debug; a single end-of-Partial
flush_and_submit then stalls the browser. Soft-flush every N walker
nodes (and on Partial yield) keeps ops buffers bounded while Full
still submits via present_frame.
2026-08-07 09:45:39 +02:00
Elena Torró 38b990ef90 🔧 Add exporter headless backend (#10875)
*  Add headless wasm render backend to the exporter

* ♻️ Move render-wasm bridge to common and split wasm builds

* 🔧 Upload builtin font variants in the wasm exporter

* ♻️ Move shared font and resources utils out of render_wasm

*  Fetch only the exported roots in the wasm exporter

*  Bound save_layer rects in the vector export path
2026-08-06 16:13:06 +02:00
Alejandro Alonso a76401596e Skip imperceptible shadows and simplify low-scale strokes (#11102)
*  Skip drop shadows that are imperceptible at current scale

Filter drop shadows by on-screen footprint (stricter for recursive
shapes) so overview HQ avoids expensive blur passes that barely show.

*  Simplify Path and Bool strokes at low scale

At overview zooms, Inner/Outer strokes fall back to Center and
dash/dotted styles become solid when the pattern is subpixel.
Strokes are never skipped so stroke-only icons stay visible.

*  Drain GPU work on partial render frames

Partial frames only flushed the Backbuffer, so tile GPU commands
queued until present_frame's flush_and_submit and stalled the
browser on large files. Submit the context each partial frame
without presenting Target or re-composing the tile atlas.

*  Prefer direct painting when effects are imperceptible

Skip the Fills/Strokes layered path when drop/inner shadows would
not paint at the current scale, and allow stroke-only shapes
(fills_none) on the direct path. Apply the same footprint LOD to
inner-shadow painting.
2026-08-06 15:58:11 +02:00
Elena Torró 10a2c19f92 🔧 Improve text editor selection and tab conversion (#11071) 2026-08-06 09:43:02 +02:00
Alejandro Alonso 4b413299c2 Clear dirty flags after tile surface reset (#11095)
Marking intermediate surfaces dirty after clearing them on tile
context switch made the first stack composite blit empty
Fills/Strokes/shadows into Current. Dirty means content to
composite, so clear the flags after the clear instead.
2026-08-06 09:09:09 +02:00
Alejandro Alonso 35bdcde183 Avoid per-tile image_snapshot when filling atlases (#11093)
Copy Current into DocAtlas and the tile atlas with Surface::draw
instead of image_snapshot_with_bounds, matching the interactive
path and removing a GPU sync stall on every completed tile.
2026-08-05 17:12:09 +02:00
Elena Torró 0fed63eeb3 🐛 Fix text replacement on selection and text offsets (#10983)
* 🐛 Fix text not being replaced when there is a selection

* 🐛 Fix text editor offsets on transformed text
2026-08-03 15:22:49 +02:00
Elena Torró a722a33503 🐛 Fix gradients and text selection (#10941)
* 🐛 Add missing text fills

*  Keep text leaf selected while editing
2026-07-30 12:11:53 +02:00
Belén Albeza 704cd40182 🐛 Fix caret not mimicking text color (#10866) 2026-07-28 16:25:47 +02:00
Elena Torró d4d22ff1a4 🔧 Prepare wasm render for headless exporter (#10795)
* 🔧 Support background blur on PDF render for strokes and text shapes

*  Add LRU eviction to the wasm image store

*  Add RasterFormat to encode png, jpeg and webp from wasm

* 🐛 Fix client-side wasm export encoding jpeg and webp as png
2026-07-28 11:54:22 +02:00
Belén Albeza 4f46700c94 🐛 Fix text editor v3 blinks (#10654) 2026-07-24 09:14:59 +02:00
Elena Torró 4383cf183a Add background blur to strokes (#10716) 2026-07-22 17:07:16 +02:00
Elena Torró ef3511e519 Add basic individual strokes implementation (#10648) 2026-07-22 16:13:38 +02:00
Elena Torró 08e42687d1 ♻️ Extract GPU-free RenderResources; add headless wasm exports (#10653) 2026-07-22 09:56:21 +02:00
Alejandro Alonso 6c5618025d Merge pull request #10712 from penpot/elenatorro-10706-apply-background-blur-to-text-shapes
 Add background blur to text shapes
2026-07-21 09:53:01 +02:00
Elena Torró 8d98877ba7 🐛 Fix using SVG image file as shape fill (#10707) 2026-07-20 10:41:07 +02:00
Elena Torro 001dba1bef Add background blur to text shapes 2026-07-16 08:33:36 +02:00
Belén Albeza 2e32a8743e ♻️ Refactor rulers into ui modules (#10461)
* ♻️ Move rulers rendering to ui submodule

* ♻️ Refactor RulerState into UIState
2026-07-03 08:37:22 +02:00
Alejandro Alonso b82ab0c830 Merge remote-tracking branch 'origin/staging' into develop 2026-07-02 13:52:25 +02:00
Elena Torró 28b5371901 🔧 Improve text strokes in texts with emoji (#10519) 2026-07-01 18:10:46 +02:00
Alejandro Alonso f25317ac47 🐛 Fix text inner stroke fill seam by compositing fill inside SrcIn (#10509)
* 🐛 Fix text inner stroke fill seam by compositing fill inside SrcIn

* 🔧 Update e2e Playwright screenshots
2026-07-01 16:24:23 +02:00