LibWeb/Canvas: Support globalCompositionOperation

Canvas now supports compositing and various blending modes via the
`globalCompositeOperation` attribute.
This commit is contained in:
Glenn Skrzypczak 2025-01-28 18:19:30 +01:00 committed by Sam Atkins
parent 19ab213b16
commit 8575bddfe6
Notes: github-actions[bot] 2025-02-05 11:28:07 +00:00
16 changed files with 283 additions and 32 deletions

View file

@ -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
};
}

View file

@ -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();
@ -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();

View file

@ -481,7 +481,7 @@ 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);
});
}
@ -489,7 +489,7 @@ void TinyVGDecodedImageData::draw(Painter& painter) const
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);
});
}
}

View file

@ -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>
@ -24,15 +25,15 @@ public:
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;

View file

@ -9,6 +9,7 @@
#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>
@ -16,6 +17,7 @@
#include <LibGfx/SkiaUtils.h>
#include <AK/TypeCasts.h>
#include <core/SkBlender.h>
#include <core/SkCanvas.h>
#include <core/SkPath.h>
#include <effects/SkBlurMaskFilter.h>
@ -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(),
@ -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)
@ -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)
@ -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);
}
@ -212,18 +217,19 @@ 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));
@ -231,6 +237,7 @@ void PainterSkia::fill_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_
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);
}

View file

@ -8,6 +8,7 @@
#include <AK/NonnullOwnPtr.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/CompositingAndBlendingOperator.h>
#include <LibGfx/Painter.h>
#include <LibGfx/PaintingSurface.h>
@ -20,13 +21,13 @@ public:
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;

View file

@ -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 {
@ -140,4 +143,85 @@ sk_sp<SkImageFilter> to_skia_image_filter(Gfx::Filter const& filter)
});
}
sk_sp<SkBlender> to_skia_blender(Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
{
switch (compositing_and_blending_operator) {
case CompositingAndBlendingOperator::Normal:
return SkBlender::Mode(SkBlendMode::kSrcOver);
case CompositingAndBlendingOperator::Multiply:
return SkBlender::Mode(SkBlendMode::kMultiply);
case CompositingAndBlendingOperator::Screen:
return SkBlender::Mode(SkBlendMode::kScreen);
case CompositingAndBlendingOperator::Overlay:
return SkBlender::Mode(SkBlendMode::kOverlay);
case CompositingAndBlendingOperator::Darken:
return SkBlender::Mode(SkBlendMode::kDarken);
case CompositingAndBlendingOperator::Lighten:
return SkBlender::Mode(SkBlendMode::kLighten);
case CompositingAndBlendingOperator::ColorDodge:
return SkBlender::Mode(SkBlendMode::kColorDodge);
case CompositingAndBlendingOperator::ColorBurn:
return SkBlender::Mode(SkBlendMode::kColorBurn);
case CompositingAndBlendingOperator::HardLight:
return SkBlender::Mode(SkBlendMode::kHardLight);
case CompositingAndBlendingOperator::SoftLight:
return SkBlender::Mode(SkBlendMode::kSoftLight);
case CompositingAndBlendingOperator::Difference:
return SkBlender::Mode(SkBlendMode::kDifference);
case CompositingAndBlendingOperator::Exclusion:
return SkBlender::Mode(SkBlendMode::kExclusion);
case CompositingAndBlendingOperator::Hue:
return SkBlender::Mode(SkBlendMode::kHue);
case CompositingAndBlendingOperator::Saturation:
return SkBlender::Mode(SkBlendMode::kSaturation);
case CompositingAndBlendingOperator::Color:
return SkBlender::Mode(SkBlendMode::kColor);
case CompositingAndBlendingOperator::Luminosity:
return SkBlender::Mode(SkBlendMode::kLuminosity);
case CompositingAndBlendingOperator::Clear:
return SkBlender::Mode(SkBlendMode::kClear);
case CompositingAndBlendingOperator::Copy:
return SkBlender::Mode(SkBlendMode::kSrc);
case CompositingAndBlendingOperator::SourceOver:
return SkBlender::Mode(SkBlendMode::kSrcOver);
case CompositingAndBlendingOperator::DestinationOver:
return SkBlender::Mode(SkBlendMode::kDstOver);
case CompositingAndBlendingOperator::SourceIn:
return SkBlender::Mode(SkBlendMode::kSrcIn);
case CompositingAndBlendingOperator::DestinationIn:
return SkBlender::Mode(SkBlendMode::kDstIn);
case CompositingAndBlendingOperator::SourceOut:
return SkBlender::Mode(SkBlendMode::kSrcOut);
case CompositingAndBlendingOperator::DestinationOut:
return SkBlender::Mode(SkBlendMode::kDstOut);
case CompositingAndBlendingOperator::SourceATop:
return SkBlender::Mode(SkBlendMode::kSrcATop);
case CompositingAndBlendingOperator::DestinationATop:
return SkBlender::Mode(SkBlendMode::kDstATop);
case CompositingAndBlendingOperator::Xor:
return SkBlender::Mode(SkBlendMode::kXor);
case CompositingAndBlendingOperator::Lighter:
return SkBlender::Mode(SkBlendMode::kPlus);
case 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);
case 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);
default:
VERIFY_NOT_REACHED();
}
}
}

View file

@ -9,6 +9,7 @@
#include <AK/Assertions.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/CompositingAndBlendingOperator.h>
#include <LibGfx/Filter.h>
#include <LibGfx/PathSkia.h>
#include <LibGfx/ScalingMode.h>
@ -18,6 +19,7 @@
#include <core/SkImageFilter.h>
#include <core/SkPath.h>
#include <core/SkSamplingOptions.h>
#include <include/core/SkBlender.h>
namespace Gfx {
@ -90,5 +92,5 @@ constexpr SkSamplingOptions to_skia_sampling_options(Gfx::ScalingMode scaling_mo
SkPath to_skia_path(Path const& path);
sk_sp<SkImageFilter> to_skia_image_filter(Gfx::Filter const& filter);
sk_sp<SkBlender> to_skia_blender(Gfx::CompositingAndBlendingOperator compositing_and_blending_operator);
}

View file

@ -18,6 +18,9 @@ public:
virtual float global_alpha() const = 0;
virtual void set_global_alpha(float) = 0;
virtual String global_composite_operation() const = 0;
virtual void set_global_composite_operation(String) = 0;
protected:
CanvasCompositing() = default;
};

View file

@ -2,5 +2,5 @@
interface mixin CanvasCompositing {
// compositing
attribute unrestricted double globalAlpha; // (default 1.0)
[FIXME] attribute DOMString globalCompositeOperation; // (default "source-over")
attribute DOMString globalCompositeOperation; // (default "source-over")
};

View file

@ -11,6 +11,7 @@
#include <AK/Vector.h>
#include <LibGfx/AffineTransform.h>
#include <LibGfx/Color.h>
#include <LibGfx/CompositingAndBlendingOperator.h>
#include <LibGfx/Filter.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/PaintStyle.h>
@ -96,6 +97,7 @@ public:
bool image_smoothing_enabled { true };
Bindings::ImageSmoothingQuality image_smoothing_quality { Bindings::ImageSmoothingQuality::Low };
float global_alpha = { 1 };
Gfx::CompositingAndBlendingOperator current_compositing_and_blending_operator = Gfx::CompositingAndBlendingOperator::SourceOver;
RefPtr<CSS::CSSStyleValue> font_style_value { nullptr };
RefPtr<Gfx::Font const> current_font { nullptr };
Bindings::CanvasTextAlign text_align { Bindings::CanvasTextAlign::Start };

View file

@ -9,6 +9,7 @@
*/
#include <AK/OwnPtr.h>
#include <LibGfx/CompositingAndBlendingOperator.h>
#include <LibGfx/PainterSkia.h>
#include <LibGfx/Rect.h>
#include <LibUnicode/Segmenter.h>
@ -178,7 +179,7 @@ WebIDL::ExceptionOr<void> CanvasRenderingContext2D::draw_image_internal(CanvasIm
}
if (auto* painter = this->painter()) {
painter->draw_bitmap(destination_rect, *bitmap, source_rect.to_rounded<int>(), scaling_mode, drawing_state().filters, drawing_state().global_alpha);
painter->draw_bitmap(destination_rect, *bitmap, source_rect.to_rounded<int>(), scaling_mode, drawing_state().filters, drawing_state().global_alpha, drawing_state().current_compositing_and_blending_operator);
did_draw(destination_rect);
}
@ -302,7 +303,7 @@ void CanvasRenderingContext2D::stroke_internal(Gfx::Path const& path)
auto& state = drawing_state();
// FIXME: Honor state's line_cap, line_join, miter_limit, dash_list, and line_dash_offset.
painter->stroke_path(path, state.stroke_style.to_gfx_paint_style(), state.filters, state.line_width, state.global_alpha);
painter->stroke_path(path, state.stroke_style.to_gfx_paint_style(), state.filters, state.line_width, state.global_alpha, state.current_compositing_and_blending_operator);
did_draw(path.bounding_box());
}
@ -338,7 +339,7 @@ void CanvasRenderingContext2D::fill_internal(Gfx::Path const& path, Gfx::Winding
auto path_to_fill = path;
path_to_fill.close_all_subpaths();
auto& state = this->drawing_state();
painter->fill_path(path_to_fill, state.fill_style.to_gfx_paint_style(), state.filters, state.global_alpha, winding_rule);
painter->fill_path(path_to_fill, state.fill_style.to_gfx_paint_style(), state.filters, state.global_alpha, state.current_compositing_and_blending_operator, winding_rule);
did_draw(path_to_fill.bounding_box());
}
@ -430,7 +431,7 @@ WebIDL::ExceptionOr<GC::Ptr<ImageData>> CanvasRenderingContext2D::get_image_data
ASSERT(image_data->bitmap().alpha_type() == Gfx::AlphaType::Unpremultiplied);
auto painter = Gfx::Painter::create(image_data->bitmap());
painter->draw_bitmap(image_data->bitmap().rect().to_type<float>(), *snapshot, source_rect_intersected, Gfx::ScalingMode::NearestNeighbor, {}, drawing_state().global_alpha);
painter->draw_bitmap(image_data->bitmap().rect().to_type<float>(), *snapshot, source_rect_intersected, Gfx::ScalingMode::NearestNeighbor, {}, drawing_state().global_alpha, Gfx::CompositingAndBlendingOperator::SourceOver);
// 7. Set the pixels values of imageData for areas of the source rectangle that are outside of the output bitmap to transparent black.
// NOTE: No-op, already done during creation.
@ -443,7 +444,7 @@ void CanvasRenderingContext2D::put_image_data(ImageData const& image_data, float
{
if (auto* painter = this->painter()) {
auto dst_rect = Gfx::FloatRect(x, y, image_data.width(), image_data.height());
painter->draw_bitmap(dst_rect, Gfx::ImmutableBitmap::create(image_data.bitmap()), image_data.bitmap().rect(), Gfx::ScalingMode::NearestNeighbor, drawing_state().filters, 1.0f);
painter->draw_bitmap(dst_rect, Gfx::ImmutableBitmap::create(image_data.bitmap()), image_data.bitmap().rect(), Gfx::ScalingMode::NearestNeighbor, drawing_state().filters, 1.0f, Gfx::CompositingAndBlendingOperator::SourceOver);
did_draw(dst_rect);
}
}
@ -751,6 +752,68 @@ void CanvasRenderingContext2D::set_global_alpha(float alpha)
drawing_state().global_alpha = alpha;
}
#define ENUMERATE_COMPOSITE_OPERATIONS(E) \
E("normal", Normal) \
E("multiply", Multiply) \
E("screen", Screen) \
E("overlay", Overlay) \
E("darken", Darken) \
E("lighten", Lighten) \
E("color-dodge", ColorDodge) \
E("color-burn", ColorBurn) \
E("hard-light", HardLight) \
E("soft-light", SoftLight) \
E("difference", Difference) \
E("exclusion", Exclusion) \
E("hue", Hue) \
E("saturation", Saturation) \
E("color", Color) \
E("luminosity", Luminosity) \
E("clear", Clear) \
E("copy", Copy) \
E("source-over", SourceOver) \
E("destination-over", DestinationOver) \
E("source-in", SourceIn) \
E("destination-in", DestinationIn) \
E("source-out", SourceOut) \
E("destination-out", DestinationOut) \
E("source-atop", SourceATop) \
E("destination-atop", DestinationATop) \
E("xor", Xor) \
E("lighter", Lighter) \
E("plus-darker", PlusDarker) \
E("plus-lighter", PlusLighter)
String CanvasRenderingContext2D::global_composite_operation() const
{
auto current_compositing_and_blending_operator = drawing_state().current_compositing_and_blending_operator;
switch (current_compositing_and_blending_operator) {
#undef __ENUMERATE
#define __ENUMERATE(operation, compositing_and_blending_operator) \
case Gfx::CompositingAndBlendingOperator::compositing_and_blending_operator: \
return #operation##_string;
ENUMERATE_COMPOSITE_OPERATIONS(__ENUMERATE)
#undef __ENUMERATE
default:
VERIFY_NOT_REACHED();
}
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-globalcompositeoperation
void CanvasRenderingContext2D::set_global_composite_operation(String global_composite_operation)
{
// 1. If the given value is not identical to any of the values that the <blend-mode> or the <composite-mode> properties are defined to take, then return.
// 2. Otherwise, set this's current compositing and blending operator to the given value.
#undef __ENUMERATE
#define __ENUMERATE(operation, compositing_and_blending_operator) \
if (global_composite_operation == operation##sv) { \
drawing_state().current_compositing_and_blending_operator = Gfx::CompositingAndBlendingOperator::compositing_and_blending_operator; \
return; \
}
ENUMERATE_COMPOSITE_OPERATIONS(__ENUMERATE)
#undef __ENUMERATE
}
float CanvasRenderingContext2D::shadow_offset_x() const
{
return drawing_state().shadow_offset_x;
@ -822,12 +885,15 @@ void CanvasRenderingContext2D::paint_shadow_for_fill_internal(Gfx::Path const& p
auto& state = this->drawing_state();
if (state.current_compositing_and_blending_operator == Gfx::CompositingAndBlendingOperator::Copy)
return;
painter->save();
Gfx::AffineTransform transform;
transform.translate(state.shadow_offset_x, state.shadow_offset_y);
painter->set_transform(transform);
painter->fill_path(path_to_fill, state.shadow_color.with_opacity(state.global_alpha), winding_rule, state.shadow_blur);
painter->fill_path(path_to_fill, state.shadow_color.with_opacity(state.global_alpha), winding_rule, state.shadow_blur, state.current_compositing_and_blending_operator);
painter->restore();
@ -842,12 +908,15 @@ void CanvasRenderingContext2D::paint_shadow_for_stroke_internal(Gfx::Path const&
auto& state = drawing_state();
if (state.current_compositing_and_blending_operator == Gfx::CompositingAndBlendingOperator::Copy)
return;
painter->save();
Gfx::AffineTransform transform;
transform.translate(state.shadow_offset_x, state.shadow_offset_y);
painter->set_transform(transform);
painter->stroke_path(path, state.shadow_color.with_opacity(state.global_alpha), state.line_width, state.shadow_blur);
painter->stroke_path(path, state.shadow_color.with_opacity(state.global_alpha), state.line_width, state.shadow_blur, state.current_compositing_and_blending_operator);
painter->restore();
@ -933,5 +1002,4 @@ void CanvasRenderingContext2D::set_filter(String filter)
// 3. If parsedValue is failure, then return.
}
}

View file

@ -103,6 +103,9 @@ public:
virtual float global_alpha() const override;
virtual void set_global_alpha(float) override;
virtual String global_composite_operation() const override;
virtual void set_global_composite_operation(String) override;
virtual String filter() const override;
virtual void set_filter(String) override;

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Canvas test: 2d.composite.transparent.lighter

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<meta charset="UTF-8">
<title>Canvas test: 2d.composite.transparent.lighter</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.composite.transparent.lighter</h1>
<p class="desc"></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="2d.composite.transparent.lighter.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("");
_addTest(function(canvas, ctx) {
ctx.fillStyle = 'rgba(0, 255, 0, 0.5)';
ctx.fillRect(0, 0, 100, 50);
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = 'rgba(0, 0, 255, 0.75)';
ctx.fillRect(0, 0, 100, 50);
_assertPixelApprox(canvas, 50,25, 0,128,191,255, 5);
});
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B