Skip to content

Commit

Permalink
LibWeb/Canvas: Support globalCompositionOperation
Browse files Browse the repository at this point in the history
Canvas now supports compositing and various blending modes via the
`globalCompositeOperation` attribute.
  • Loading branch information
skyz1 committed Jan 28, 2025
1 parent 8b5a25e commit 357331b
Show file tree
Hide file tree
Showing 16 changed files with 311 additions and 32 deletions.
44 changes: 44 additions & 0 deletions Libraries/LibGfx/CompositingAndBlendingOperator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025, Glenn Skrzypczak <glenn.skrzypczak@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#pragma once

namespace Gfx {

enum class CompositingAndBlendingOperator {
Normal,
Multiply,
Screen,
Overlay,
Darken,
Lighten,
ColorDodge,
ColorBurn,
HardLight,
SoftLight,
Difference,
Exclusion,
Hue,
Saturation,
Color,
Luminosity,
Clear,
Copy,
SourceOver,
DestinationOver,
SourceIn,
DestinationIn,
SourceOut,
DestinationOut,
SourceATop,
DestinationATop,
Xor,
Lighter,
PlusDarker,
PlusLighter
};

}
8 changes: 3 additions & 5 deletions Libraries/LibGfx/ImageFormats/PNGLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,11 @@ ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in
switch (blend_op) {
case PNG_BLEND_OP_SOURCE:
// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
painter->clear_rect(frame_rect, Gfx::Color::Transparent);
painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*decoded_frame_bitmap), decoded_frame_bitmap->rect(), Gfx::ScalingMode::NearestNeighbor, {}, 1.0f);
painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*decoded_frame_bitmap), decoded_frame_bitmap->rect(), Gfx::ScalingMode::NearestNeighbor, {}, 1.0f, Gfx::CompositingAndBlendingOperator::Copy);
break;
case PNG_BLEND_OP_OVER:
// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification.
painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*decoded_frame_bitmap), decoded_frame_bitmap->rect(), ScalingMode::NearestNeighbor, {}, 1.0f);
painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*decoded_frame_bitmap), decoded_frame_bitmap->rect(), ScalingMode::NearestNeighbor, {}, 1.0f, Gfx::CompositingAndBlendingOperator::SourceOver);
break;
default:
VERIFY_NOT_REACHED();
Expand All @@ -306,8 +305,7 @@ ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in
break;
case PNG_DISPOSE_OP_PREVIOUS:
// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
painter->clear_rect(frame_rect, Gfx::Color::Transparent);
painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*prev_output_buffer), IntRect { x, y, width, height }, Gfx::ScalingMode::NearestNeighbor, {}, 1.0f);
painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*prev_output_buffer), IntRect { x, y, width, height }, Gfx::ScalingMode::NearestNeighbor, {}, 1.0f, Gfx::CompositingAndBlendingOperator::Copy);
break;
default:
VERIFY_NOT_REACHED();
Expand Down
4 changes: 2 additions & 2 deletions Libraries/LibGfx/ImageFormats/TinyVGLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -481,15 +481,15 @@ void TinyVGDecodedImageData::draw(Painter& painter) const
command.fill->visit(
[&](Color color) { painter.fill_path(fill_path, color, WindingRule::EvenOdd); },
[&](NonnullRefPtr<SVGGradientPaintStyle> const& style) {
painter.fill_path(fill_path, style, {}, 1.0f, WindingRule::EvenOdd);
painter.fill_path(fill_path, style, {}, 1.0f, CompositingAndBlendingOperator::SourceOver, WindingRule::EvenOdd);
});
}

if (command.stroke.has_value()) {
command.stroke->visit(
[&](Color color) { painter.stroke_path(draw_path, color, command.stroke_width); },
[&](NonnullRefPtr<SVGGradientPaintStyle> const& style) {
painter.stroke_path(draw_path, style, {}, command.stroke_width, 1.0f);
painter.stroke_path(draw_path, style, {}, command.stroke_width, 1.0f, CompositingAndBlendingOperator::SourceOver);
});
}
}
Expand Down
11 changes: 6 additions & 5 deletions Libraries/LibGfx/Painter.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#pragma once

#include <AK/Forward.h>
#include <LibGfx/CompositingAndBlendingOperator.h>
#include <LibGfx/Filter.h>
#include <LibGfx/Forward.h>
#include <LibGfx/PaintStyle.h>
Expand All @@ -24,15 +25,15 @@ class Painter {
virtual void clear_rect(Gfx::FloatRect const&, Gfx::Color) = 0;
virtual void fill_rect(Gfx::FloatRect const&, Gfx::Color) = 0;

virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, ReadonlySpan<Gfx::Filter> filters, float global_alpha) = 0;
virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, ReadonlySpan<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) = 0;
virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan<Gfx::Filter>, float thickness, float global_alpha) = 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::PaintStyle const&, ReadonlySpan<Gfx::Filter>, float thickness, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) = 0;

virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule) = 0;
virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule, float blur_radius) = 0;
virtual void fill_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan<Gfx::Filter>, float global_alpha, Gfx::WindingRule) = 0;
virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) = 0;
virtual void fill_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan<Gfx::Filter>, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::WindingRule) = 0;

virtual void set_transform(Gfx::AffineTransform const&) = 0;

Expand Down
17 changes: 12 additions & 5 deletions Libraries/LibGfx/PainterSkia.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
#define AK_DONT_REPLACE_STD

#include <AK/OwnPtr.h>
#include <AK/String.h>
#include <LibGfx/Filter.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibGfx/PainterSkia.h>
#include <LibGfx/PathSkia.h>
#include <LibGfx/SkiaUtils.h>

#include <AK/TypeCasts.h>
#include <core/SkBlender.h>
#include <core/SkCanvas.h>
#include <core/SkPath.h>
#include <effects/SkBlurMaskFilter.h>
Expand Down Expand Up @@ -130,11 +132,12 @@ void PainterSkia::fill_rect(Gfx::FloatRect const& rect, Color color)
impl().canvas()->drawRect(to_skia_rect(rect), paint);
}

void PainterSkia::draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode scaling_mode, ReadonlySpan<Gfx::Filter> filters, float global_alpha)
void PainterSkia::draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode scaling_mode, ReadonlySpan<Gfx::Filter> filters, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
{
SkPaint paint;
apply_filters(paint, filters);
paint.setAlpha(static_cast<u8>(global_alpha * 255));
paint.setBlender(to_skia_blender(compositing_and_blending_operator));

impl().canvas()->drawImageRect(
src_bitmap.sk_image(),
Expand Down Expand Up @@ -170,7 +173,7 @@ void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thi
impl().canvas()->drawPath(sk_path, paint);
}

void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thickness, float blur_radius)
void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thickness, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
{
// Skia treats zero thickness as a special case and will draw a hairline, while we want to draw nothing.
if (thickness <= 0)
Expand All @@ -182,11 +185,12 @@ 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.setBlender(to_skia_blender(compositing_and_blending_operator));
auto sk_path = to_skia_path(path);
impl().canvas()->drawPath(sk_path, paint);
}

void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, ReadonlySpan<Gfx::Filter> filters, float thickness, float global_alpha)
void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, ReadonlySpan<Gfx::Filter> filters, float thickness, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
{
// Skia treats zero thickness as a special case and will draw a hairline, while we want to draw nothing.
if (thickness <= 0)
Expand All @@ -199,6 +203,7 @@ void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::PaintStyle const& pain
paint.setAlphaf(alpha * global_alpha);
paint.setStyle(SkPaint::Style::kStroke_Style);
paint.setStrokeWidth(thickness);
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
impl().canvas()->drawPath(sk_path, paint);
}

Expand All @@ -212,25 +217,27 @@ void PainterSkia::fill_path(Gfx::Path const& path, Gfx::Color color, Gfx::Windin
impl().canvas()->drawPath(sk_path, paint);
}

void PainterSkia::fill_path(Gfx::Path const& path, Gfx::Color color, Gfx::WindingRule winding_rule, float blur_radius)
void PainterSkia::fill_path(Gfx::Path const& path, Gfx::Color color, Gfx::WindingRule winding_rule, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
{
SkPaint paint;
paint.setAntiAlias(true);
paint.setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, blur_radius / 2));
paint.setColor(to_skia_color(color));
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
auto sk_path = to_skia_path(path);
sk_path.setFillType(to_skia_path_fill_type(winding_rule));
impl().canvas()->drawPath(sk_path, paint);
}

void PainterSkia::fill_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, ReadonlySpan<Gfx::Filter> filters, float global_alpha, Gfx::WindingRule winding_rule)
void PainterSkia::fill_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, ReadonlySpan<Gfx::Filter> filters, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::WindingRule winding_rule)
{
auto sk_path = to_skia_path(path);
sk_path.setFillType(to_skia_path_fill_type(winding_rule));
auto paint = to_skia_paint(paint_style, filters);
paint.setAntiAlias(true);
float alpha = paint.getAlphaf();
paint.setAlphaf(alpha * global_alpha);
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
impl().canvas()->drawPath(sk_path, paint);
}

Expand Down
11 changes: 6 additions & 5 deletions Libraries/LibGfx/PainterSkia.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <AK/NonnullOwnPtr.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/CompositingAndBlendingOperator.h>
#include <LibGfx/Painter.h>
#include <LibGfx/PaintingSurface.h>

Expand All @@ -20,13 +21,13 @@ class PainterSkia final : public Painter {

virtual void clear_rect(Gfx::FloatRect const&, Color) override;
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, ReadonlySpan<Gfx::Filter>, float global_alpha) override;
virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, ReadonlySpan<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) override;
virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan<Gfx::Filter>, float thickness, float global_alpha) 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::PaintStyle const&, ReadonlySpan<Gfx::Filter>, float thickness, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) override;
virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule) override;
virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule, float blur_radius) override;
virtual void fill_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan<Gfx::Filter>, float global_alpha, Gfx::WindingRule) override;
virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator) override;
virtual void fill_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan<Gfx::Filter>, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::WindingRule) override;
virtual void set_transform(Gfx::AffineTransform const&) override;
virtual void save() override;
virtual void restore() override;
Expand Down
112 changes: 112 additions & 0 deletions Libraries/LibGfx/SkiaUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
#include <AK/Assertions.h>
#include <LibGfx/Filter.h>
#include <LibGfx/SkiaUtils.h>
#include <core/SkBlender.h>
#include <core/SkColorFilter.h>
#include <core/SkImageFilter.h>
#include <core/SkString.h>
#include <effects/SkImageFilters.h>
#include <effects/SkRuntimeEffect.h>

namespace Gfx {

Expand Down Expand Up @@ -140,4 +143,113 @@ sk_sp<SkImageFilter> to_skia_image_filter(Gfx::Filter const& filter)
});
}

sk_sp<SkBlender> to_skia_blender(Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
{
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Normal) {
return SkBlender::Mode(SkBlendMode::kSrcOver);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Multiply) {
return SkBlender::Mode(SkBlendMode::kMultiply);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Screen) {
return SkBlender::Mode(SkBlendMode::kScreen);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Overlay) {
return SkBlender::Mode(SkBlendMode::kOverlay);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Darken) {
return SkBlender::Mode(SkBlendMode::kDarken);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Lighten) {
return SkBlender::Mode(SkBlendMode::kLighten);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::ColorDodge) {
return SkBlender::Mode(SkBlendMode::kColorDodge);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::ColorBurn) {
return SkBlender::Mode(SkBlendMode::kColorBurn);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::HardLight) {
return SkBlender::Mode(SkBlendMode::kHardLight);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::SoftLight) {
return SkBlender::Mode(SkBlendMode::kSoftLight);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Difference) {
return SkBlender::Mode(SkBlendMode::kDifference);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Exclusion) {
return SkBlender::Mode(SkBlendMode::kExclusion);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Hue) {
return SkBlender::Mode(SkBlendMode::kHue);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Saturation) {
return SkBlender::Mode(SkBlendMode::kSaturation);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Color) {
return SkBlender::Mode(SkBlendMode::kColor);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Luminosity) {
return SkBlender::Mode(SkBlendMode::kLuminosity);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Clear) {
return SkBlender::Mode(SkBlendMode::kClear);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Copy) {
return SkBlender::Mode(SkBlendMode::kSrc);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::SourceOver) {
return SkBlender::Mode(SkBlendMode::kSrcOver);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::DestinationOver) {
return SkBlender::Mode(SkBlendMode::kDstOver);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::SourceIn) {
return SkBlender::Mode(SkBlendMode::kSrcIn);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::DestinationIn) {
return SkBlender::Mode(SkBlendMode::kDstIn);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::SourceOut) {
return SkBlender::Mode(SkBlendMode::kSrcOut);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::DestinationOut) {
return SkBlender::Mode(SkBlendMode::kDstOut);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::SourceATop) {
return SkBlender::Mode(SkBlendMode::kSrcATop);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::DestinationATop) {
return SkBlender::Mode(SkBlendMode::kDstATop);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Xor) {
return SkBlender::Mode(SkBlendMode::kXor);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::Lighter) {
return SkBlender::Mode(SkBlendMode::kPlus);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::PlusDarker) {
// https://drafts.fxtf.org/compositing/#porterduffcompositingoperators_plus_darker
// FIXME: This does not match the spec, however it looks like Safari, the only popular browser supporting this operator.
return SkRuntimeEffect::MakeForBlender(SkString(R"(
vec4 main(vec4 source, vec4 destination) {
return saturate(saturate(destination.a + source.a) - saturate(destination.a - destination) - saturate(source.a - source));
}
)"))
.effect->makeBlender(nullptr);
}
if (compositing_and_blending_operator == CompositingAndBlendingOperator::PlusLighter) {
// https://drafts.fxtf.org/compositing/#porterduffcompositingoperators_plus_lighter
return SkRuntimeEffect::MakeForBlender(SkString(R"(
vec4 main(vec4 source, vec4 destination) {
return saturate(source + destination);
}
)"))
.effect->makeBlender(nullptr);
}

ASSERT_NOT_REACHED();
}

}
Loading

0 comments on commit 357331b

Please sign in to comment.