LibWeb+LibGfx: Draw shadows for stroke joins and caps

This commit is contained in:
Tim Ledbetter 2025-10-21 13:41:11 +01:00 committed by Jelle Raaijmakers
parent 0f295e8989
commit 7db73118e9
Notes: github-actions[bot] 2025-10-21 16:56:30 +00:00
15 changed files with 257 additions and 8 deletions

View file

@ -29,7 +29,7 @@ public:
virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, Optional<Gfx::Filter> filters, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) = 0;
virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness) = 0;
virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) = 0;
virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::Path::CapStyle, Gfx::Path::JoinStyle, float miter_limit, Vector<float> const& dash_array, float dash_offset) = 0;
virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, Optional<Gfx::Filter>, float thickness, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) = 0;
virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, Optional<Gfx::Filter>, float thickness, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::Path::CapStyle const&, Gfx::Path::JoinStyle const&, float miter_limit, Vector<float> const&, float dash_offset) = 0;

View file

@ -188,7 +188,7 @@ void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thi
});
}
void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thickness, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thickness, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::Path::CapStyle cap_style, Gfx::Path::JoinStyle join_style, float miter_limit, Vector<float> const& dash_array, float dash_offset)
{
// Skia treats zero thickness as a special case and will draw a hairline, while we want to draw nothing.
if (thickness <= 0)
@ -200,6 +200,10 @@ void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thi
paint.setStyle(SkPaint::kStroke_Style);
paint.setStrokeWidth(thickness);
paint.setColor(to_skia_color(color));
paint.setStrokeCap(to_skia_cap(cap_style));
paint.setStrokeJoin(to_skia_join(join_style));
paint.setStrokeMiter(miter_limit);
paint.setPathEffect(SkDashPathEffect::Make(dash_array.data(), dash_array.size(), dash_offset));
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
auto sk_path = to_skia_path(path);
impl().with_canvas([&](auto& canvas) {

View file

@ -23,7 +23,7 @@ public:
virtual void fill_rect(Gfx::FloatRect const&, Color) override;
virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, Optional<Gfx::Filter>, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) override;
virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness) override;
virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) override;
virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::Path::CapStyle, Gfx::Path::JoinStyle, float miter_limit, Vector<float> const& dash_array, float dash_offset) override;
virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, Optional<Gfx::Filter>, float thickness, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) override;
virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, Optional<Gfx::Filter>, float thickness, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::Path::CapStyle const&, Gfx::Path::JoinStyle const&, float miter_limit, Vector<float> const&, float dash_offset) override;
virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule) override;

View file

@ -373,8 +373,6 @@ void CanvasRenderingContext2D::stroke_internal(Gfx::Path const& path)
if (!painter)
return;
paint_shadow_for_stroke_internal(path);
auto& state = drawing_state();
auto line_cap = to_gfx_cap(state.line_cap);
@ -386,6 +384,7 @@ void CanvasRenderingContext2D::stroke_internal(Gfx::Path const& path)
for (auto const& dash : state.dash_list) {
dash_array.append(static_cast<float>(dash));
}
paint_shadow_for_stroke_internal(path, line_cap, line_join, dash_array);
painter->stroke_path(path, state.stroke_style.to_gfx_paint_style(), state.filter, state.line_width, state.global_alpha, state.current_compositing_and_blending_operator, line_cap, line_join, state.miter_limit, dash_array, state.line_dash_offset);
did_draw(path.bounding_box());
@ -1093,7 +1092,7 @@ void CanvasRenderingContext2D::paint_shadow_for_fill_internal(Gfx::Path const& p
did_draw(path.bounding_box());
}
void CanvasRenderingContext2D::paint_shadow_for_stroke_internal(Gfx::Path const& path)
void CanvasRenderingContext2D::paint_shadow_for_stroke_internal(Gfx::Path const& path, Gfx::Path::CapStyle line_cap, Gfx::Path::JoinStyle line_join, Vector<float> const& dash_array)
{
auto* painter = this->painter();
if (!painter)
@ -1123,7 +1122,7 @@ void CanvasRenderingContext2D::paint_shadow_for_stroke_internal(Gfx::Path const&
transform.translate(state.shadow_offset_x, state.shadow_offset_y);
transform.multiply(state.transform);
painter->set_transform(transform);
painter->stroke_path(path, state.shadow_color.with_opacity(alpha), state.line_width, state.shadow_blur, state.current_compositing_and_blending_operator);
painter->stroke_path(path, state.shadow_color.with_opacity(alpha), state.line_width, state.shadow_blur, state.current_compositing_and_blending_operator, line_cap, line_join, state.miter_limit, dash_array, state.line_dash_offset);
painter->restore();

View file

@ -160,7 +160,7 @@ private:
void fill_internal(Gfx::Path const&, Gfx::WindingRule);
void clip_internal(Gfx::Path&, Gfx::WindingRule);
void paint_shadow_for_fill_internal(Gfx::Path const&, Gfx::WindingRule);
void paint_shadow_for_stroke_internal(Gfx::Path const&);
void paint_shadow_for_stroke_internal(Gfx::Path const&, Gfx::Path::CapStyle, Gfx::Path::JoinStyle, Vector<float> const&);
GC::Ref<HTMLCanvasElement> m_element;
OwnPtr<Gfx::Painter> m_painter;

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Shadows are not drawn for areas outside stroke caps

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Shadows are drawn for stroke caps

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Shadows are not drawn for areas outside stroke joins

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Shadows are drawn for stroke joins

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Shadows are drawn for stroke joins respecting miter limit

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<meta charset="UTF-8">
<title>Canvas test: 2d.shadow.stroke.cap.1</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="../../../../html/canvas/resources/canvas-tests.css">
<body class="show_output">
<h1>2d.shadow.stroke.cap.1</h1>
<p class="desc">Shadows are not drawn for areas outside stroke caps</p>
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
<p class="output expectedtext">Expected output:<p><img src="../../../../images/green-100x50.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("Shadows are not drawn for areas outside stroke caps");
_addTest(function(canvas, ctx) {
ctx.fillStyle = '#0f0';
ctx.fillRect(0, 0, 100, 50);
ctx.strokeStyle = '#f00';
ctx.shadowColor = '#f00';
ctx.shadowOffsetY = 50;
ctx.beginPath();
ctx.lineWidth = 50;
ctx.lineCap = 'butt';
ctx.moveTo(-50, -25);
ctx.lineTo(0, -25);
ctx.moveTo(100, -25);
ctx.lineTo(150, -25);
ctx.stroke();
_assertPixel(canvas, 1,25, 0,255,0,255);
_assertPixel(canvas, 50,25, 0,255,0,255);
_assertPixel(canvas, 98,25, 0,255,0,255);
});
</script>

View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<meta charset="UTF-8">
<title>Canvas test: 2d.shadow.stroke.cap.2</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="../../../../html/canvas/resources/canvas-tests.css">
<body class="show_output">
<h1>2d.shadow.stroke.cap.2</h1>
<p class="desc">Shadows are drawn for stroke caps</p>
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
<p class="output expectedtext">Expected output:<p><img src="../../../../images/green-100x50.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("Shadows are drawn for stroke caps");
_addTest(function(canvas, ctx) {
ctx.fillStyle = '#f00';
ctx.fillRect(0, 0, 100, 50);
ctx.strokeStyle = '#f00';
ctx.shadowColor = '#0f0';
ctx.shadowOffsetY = 50;
ctx.beginPath();
ctx.lineWidth = 50;
ctx.lineCap = 'square';
ctx.moveTo(25, -25);
ctx.lineTo(75, -25);
ctx.stroke();
_assertPixel(canvas, 1,25, 0,255,0,255);
_assertPixel(canvas, 50,25, 0,255,0,255);
_assertPixel(canvas, 98,25, 0,255,0,255);
});
</script>

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<meta charset="UTF-8">
<title>Canvas test: 2d.shadow.stroke.join.1</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="../../../../html/canvas/resources/canvas-tests.css">
<body class="show_output">
<h1>2d.shadow.stroke.join.1</h1>
<p class="desc">Shadows are not drawn for areas outside stroke joins</p>
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
<p class="output expectedtext">Expected output:<p><img src="../../../../images/green-100x50.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("Shadows are not drawn for areas outside stroke joins");
_addTest(function(canvas, ctx) {
ctx.fillStyle = '#0f0';
ctx.fillRect(0, 0, 100, 50);
ctx.strokeStyle = '#f00';
ctx.shadowColor = '#f00';
ctx.shadowOffsetX = 100;
ctx.lineWidth = 200;
ctx.lineJoin = 'bevel';
ctx.beginPath();
ctx.moveTo(-200, -50);
ctx.lineTo(-150, -50);
ctx.lineTo(-151, -100);
ctx.stroke();
_assertPixel(canvas, 1,1, 0,255,0,255);
_assertPixel(canvas, 48,48, 0,255,0,255);
_assertPixel(canvas, 50,25, 0,255,0,255);
_assertPixel(canvas, 98,48, 0,255,0,255);
});
</script>

View file

@ -0,0 +1,45 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<meta charset="UTF-8">
<title>Canvas test: 2d.shadow.stroke.join.2</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="../../../../html/canvas/resources/canvas-tests.css">
<body class="show_output">
<h1>2d.shadow.stroke.join.2</h1>
<p class="desc">Shadows are drawn for stroke joins</p>
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
<p class="output expectedtext">Expected output:<p><img src="../../../../images/green-100x50.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("Shadows are drawn for stroke joins");
_addTest(function(canvas, ctx) {
ctx.fillStyle = '#f00';
ctx.fillRect(0, 0, 50, 50);
ctx.fillStyle = '#0f0';
ctx.fillRect(50, 0, 50, 50);
ctx.strokeStyle = '#f00';
ctx.shadowColor = '#0f0';
ctx.shadowOffsetX = 100;
ctx.lineWidth = 200;
ctx.lineJoin = 'miter';
ctx.beginPath();
ctx.moveTo(-200, -50);
ctx.lineTo(-150, -50);
ctx.lineTo(-151, -100);
ctx.stroke();
_assertPixel(canvas, 1,1, 0,255,0,255);
_assertPixel(canvas, 48,48, 0,255,0,255);
_assertPixel(canvas, 50,25, 0,255,0,255);
_assertPixel(canvas, 98,48, 0,255,0,255);
});
</script>

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<meta charset="UTF-8">
<title>Canvas test: 2d.shadow.stroke.join.3</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="../../../../html/canvas/resources/canvas-tests.css">
<body class="show_output">
<h1>2d.shadow.stroke.join.3</h1>
<p class="desc">Shadows are drawn for stroke joins respecting miter limit</p>
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
<p class="output expectedtext">Expected output:<p><img src="../../../../images/green-100x50.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("Shadows are drawn for stroke joins respecting miter limit");
_addTest(function(canvas, ctx) {
ctx.fillStyle = '#0f0';
ctx.fillRect(0, 0, 100, 50);
ctx.strokeStyle = '#f00';
ctx.shadowColor = '#f00';
ctx.shadowOffsetX = 100;
ctx.lineWidth = 200;
ctx.lineJoin = 'miter';
ctx.miterLimit = 0.1;
ctx.beginPath();
ctx.moveTo(-200, -50);
ctx.lineTo(-150, -50);
ctx.lineTo(-151, -100); // (not an exact right angle, to avoid some other bug in Firefox 3)
ctx.stroke();
_assertPixel(canvas, 1,1, 0,255,0,255);
_assertPixel(canvas, 48,48, 0,255,0,255);
_assertPixel(canvas, 50,25, 0,255,0,255);
_assertPixel(canvas, 98,48, 0,255,0,255);
});
</script>