fix(canvas): render native linear and radial gradients

This commit is contained in:
ldm0
2026-10-05 00:30:31 +08:00
parent 2fe058a18f
commit 5f3f11bbe1
15 changed files with 743 additions and 172 deletions
+1
View File
@@ -189,6 +189,7 @@ fn linear_gradient(
stops,
extend: gradient_extend(repeating),
interpolation,
transform: LayoutTransform2D::IDENTITY,
}))
}
@@ -154,6 +154,7 @@ fn paint_interpolation(method: ColorInterpolationMethod) -> Option<PaintGradient
Some(PaintGradientInterpolation {
color_space,
hue_direction,
..Default::default()
})
}
+7 -7
View File
@@ -76,13 +76,13 @@ pub use snapshot::{
PaintBrush, PaintColor, PaintCompositeMode, PaintConicGradient, PaintCornerRadii,
PaintCornerRadius, PaintDiagnostic, PaintDiagnosticSeverity, PaintEdgeSizes, PaintFilter,
PaintFontId, PaintFontResource, PaintFragment, PaintGlyph, PaintGlyphRun,
PaintGradientColorSpace, PaintGradientExtend, PaintGradientHueDirection,
PaintGradientInterpolation, PaintGradientStop, PaintImage, PaintImageId, PaintImageResource,
PaintImageSampling, PaintLineCap, PaintLineJoin, PaintLinearGradient, PaintPath,
PaintPathElement, PaintPoint, PaintRadialGradient, PaintRect, PaintShape, PaintSize,
PaintSnapshot, PaintStroke, PaintSvgImage, PaintSvgImageId, PaintSvgImageResource,
PaintTextDecoration, PaintTextDecorationStyle, PaintTextShadow, PaintTransform2D,
PaintViewport, pixel_snap_paint_axis, pixel_snap_paint_axis_allowing_zero,
PaintGradientAlphaSpace, PaintGradientColorSpace, PaintGradientExtend,
PaintGradientHueDirection, PaintGradientInterpolation, PaintGradientStop, PaintImage,
PaintImageId, PaintImageResource, PaintImageSampling, PaintLineCap, PaintLineJoin,
PaintLinearGradient, PaintPath, PaintPathElement, PaintPoint, PaintRadialGradient, PaintRect,
PaintShape, PaintSize, PaintSnapshot, PaintStroke, PaintSvgImage, PaintSvgImageId,
PaintSvgImageResource, PaintTextDecoration, PaintTextDecorationStyle, PaintTextShadow,
PaintTransform2D, PaintViewport, pixel_snap_paint_axis, pixel_snap_paint_axis_allowing_zero,
pixel_snap_paint_rect,
};
pub use source::{
+2 -2
View File
@@ -86,7 +86,7 @@ pub(super) fn project_form_control_appearance<N>(
};
snapshot.push_fragment(PaintFragment::Stroke(PaintStroke {
path,
color: PaintColor::WHITE,
brush: PaintBrush::Solid(PaintColor::WHITE),
width: 2.0 * scale,
join: PaintLineJoin::Round,
start_cap: PaintLineCap::Round,
@@ -100,7 +100,7 @@ pub(super) fn project_form_control_appearance<N>(
fill(shape, PaintColor::WHITE);
snapshot.push_fragment(PaintFragment::Stroke(PaintStroke {
path: rounded_rect_path(rect, frame_radius),
color: accent,
brush: PaintBrush::Solid(accent),
width: 1.0,
join: PaintLineJoin::Miter,
start_cap: PaintLineCap::Butt,
+13 -1
View File
@@ -155,7 +155,7 @@ pub enum PaintLineJoin {
#[derive(Clone, Debug, PartialEq)]
pub struct PaintStroke {
pub path: PaintPath,
pub color: PaintColor,
pub brush: PaintBrush,
pub width: f32,
pub join: PaintLineJoin,
pub start_cap: PaintLineCap,
@@ -352,6 +352,16 @@ pub enum PaintGradientHueDirection {
pub struct PaintGradientInterpolation {
pub color_space: PaintGradientColorSpace,
pub hue_direction: PaintGradientHueDirection,
pub alpha_space: PaintGradientAlphaSpace,
}
/// CSS interpolates premultiplied colors; Canvas gradients interpolate straight
/// color channels before applying the interpolated alpha.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PaintGradientAlphaSpace {
#[default]
Premultiplied,
Unpremultiplied,
}
/// One resolved gradient stop. Offsets are normalized to the gradient line.
@@ -369,6 +379,8 @@ pub struct PaintLinearGradient {
pub stops: Vec<PaintGradientStop>,
pub extend: PaintGradientExtend,
pub interpolation: PaintGradientInterpolation,
/// Brush-local coordinates relative to the primitive's coordinate space.
pub transform: PaintTransform2D,
}
/// An owned, resolved two-circle radial gradient.
+1 -1
View File
@@ -2703,7 +2703,7 @@ fn form_controls_have_deterministic_intrinsic_geometry_content_and_basic_appeara
matches!(
fragment,
PaintFragment::Stroke(stroke)
if stroke.color == PaintColor::WHITE
if stroke.brush == moli_layout::PaintBrush::Solid(PaintColor::WHITE)
&& stroke.width > 0.0
&& stroke.start_cap == moli_layout::PaintLineCap::Round
&& stroke.end_cap == moli_layout::PaintLineCap::Round
+52 -31
View File
@@ -18,15 +18,16 @@ use filters::{apply_software_color_filter, backend_filter, is_software_color_fil
use moli_layout::{
PaintBlendMode, PaintBorderColors, PaintBorderStyle, PaintBorderStyles, PaintBoxShadow,
PaintBrush, PaintCaptureSurface, PaintColor, PaintCompositeMode, PaintCornerRadii,
PaintCornerRadius, PaintEdgeSizes, PaintFragment, PaintGradientColorSpace, PaintGradientExtend,
PaintGradientHueDirection, PaintImage, PaintImageSampling, PaintLineCap, PaintLineJoin,
PaintPath, PaintPathElement, PaintRect, PaintShape, PaintSnapshot, PaintStroke, PaintSvgImage,
PaintTextDecoration, PaintTextDecorationStyle, PaintTextShadow, PaintTransform2D,
pixel_snap_paint_axis, pixel_snap_paint_axis_allowing_zero, pixel_snap_paint_rect,
PaintCornerRadius, PaintEdgeSizes, PaintFragment, PaintGradientAlphaSpace,
PaintGradientColorSpace, PaintGradientExtend, PaintGradientHueDirection, PaintImage,
PaintImageSampling, PaintLineCap, PaintLineJoin, PaintPath, PaintPathElement, PaintRect,
PaintShape, PaintSnapshot, PaintStroke, PaintSvgImage, PaintTextDecoration,
PaintTextDecorationStyle, PaintTextShadow, PaintTransform2D, pixel_snap_paint_axis,
pixel_snap_paint_axis_allowing_zero, pixel_snap_paint_rect,
};
use peniko::{
BlendMode, Blob, Color, Compose, Extend, Fill, Gradient, ImageAlphaType, ImageBrush, ImageData,
ImageFormat, ImageQuality, ImageSampler, Mix,
ImageFormat, ImageQuality, ImageSampler, InterpolationAlphaSpace, Mix,
color::{ColorSpaceTag, HueDirection},
kurbo::{
Affine, BezPath, Cap, Circle, Ellipse, Insets, Join, PathEl, Point, Rect, RoundedRect,
@@ -383,7 +384,8 @@ fn fragment_auxiliary_units(snapshot: &PaintSnapshot, fragment: &PaintFragment)
.path
.elements
.len()
.saturating_add(stroke.dash_pattern.len()),
.saturating_add(stroke.dash_pattern.len())
.saturating_add(brush_units(&stroke.brush)),
PaintFragment::TextShadow(shadow) => shadow.run.normalized_coords.len(),
PaintFragment::GlyphRun(run) => run.normalized_coords.len(),
PaintFragment::SvgImage(image) => snapshot
@@ -1635,7 +1637,7 @@ fn backend_glyph_transform(skew_radians: Option<f32>) -> Option<Affine> {
fn paint_stroke(scene: &mut impl PaintScene, stroke: &PaintStroke, scale: f64) {
let width = f64::from(finite_nonnegative(stroke.width));
if width <= 0.0 || stroke.color.alpha <= 0.0 {
if width <= 0.0 {
return;
}
let mut backend_stroke = Stroke::new(width);
@@ -1654,26 +1656,32 @@ fn paint_stroke(scene: &mut impl PaintScene, stroke: &PaintStroke, scale: f64) {
.collect();
backend_stroke.dash_offset = f64::from(finite_or_zero(stroke.dash_offset));
let path = to_backend_path(&stroke.path);
let (brush, brush_transform) = backend_brush(&stroke.brush);
scene.stroke(
&backend_stroke,
device_transform(stroke.transform, scale),
to_backend_color(stroke.color),
None,
peniko::BrushRef::from(&brush),
brush_transform,
&path,
);
}
fn paint_fill(scene: &mut impl PaintScene, shape: &BezPath, brush: &PaintBrush, transform: Affine) {
let (brush, brush_transform) = backend_brush(brush);
scene.fill(
Fill::NonZero,
transform,
peniko::BrushRef::from(&brush),
brush_transform,
shape,
);
}
fn backend_brush(brush: &PaintBrush) -> (peniko::Brush, Option<Affine>) {
match brush {
PaintBrush::Solid(color) => scene.fill(
Fill::NonZero,
transform,
to_backend_color(*color),
None,
shape,
),
PaintBrush::Solid(color) => (to_backend_color(*color).into(), None),
PaintBrush::LinearGradient(gradient) => {
let gradient = Gradient::new_linear(
let gradient_brush = Gradient::new_linear(
(f64::from(gradient.start.x), f64::from(gradient.start.y)),
(f64::from(gradient.end.x), f64::from(gradient.end.y)),
)
@@ -1681,11 +1689,17 @@ fn paint_fill(scene: &mut impl PaintScene, shape: &BezPath, brush: &PaintBrush,
.with_interpolation_cs(to_backend_gradient_color_space(
gradient.interpolation.color_space,
))
.with_interpolation_alpha_space(backend_gradient_alpha_space(
gradient.interpolation.alpha_space,
))
.with_hue_direction(to_backend_gradient_hue_direction(
gradient.interpolation.hue_direction,
))
.with_stops(backend_gradient_stops(&gradient.stops).as_slice());
scene.fill(Fill::NonZero, transform, &gradient, None, shape);
(
gradient_brush.into(),
Some(to_backend_transform(gradient.transform)),
)
}
PaintBrush::RadialGradient(gradient) => {
let gradient_brush = Gradient::new_two_point_radial(
@@ -1704,17 +1718,17 @@ fn paint_fill(scene: &mut impl PaintScene, shape: &BezPath, brush: &PaintBrush,
.with_interpolation_cs(to_backend_gradient_color_space(
gradient.interpolation.color_space,
))
.with_interpolation_alpha_space(backend_gradient_alpha_space(
gradient.interpolation.alpha_space,
))
.with_hue_direction(to_backend_gradient_hue_direction(
gradient.interpolation.hue_direction,
))
.with_stops(backend_gradient_stops(&gradient.stops).as_slice());
scene.fill(
Fill::NonZero,
transform,
&gradient_brush,
(
gradient_brush.into(),
Some(to_backend_transform(gradient.transform)),
shape,
);
)
}
PaintBrush::ConicGradient(gradient) => {
let gradient_brush = Gradient::new_sweep(
@@ -1726,21 +1740,28 @@ fn paint_fill(scene: &mut impl PaintScene, shape: &BezPath, brush: &PaintBrush,
.with_interpolation_cs(to_backend_gradient_color_space(
gradient.interpolation.color_space,
))
.with_interpolation_alpha_space(backend_gradient_alpha_space(
gradient.interpolation.alpha_space,
))
.with_hue_direction(to_backend_gradient_hue_direction(
gradient.interpolation.hue_direction,
))
.with_stops(backend_gradient_stops(&gradient.stops).as_slice());
scene.fill(
Fill::NonZero,
transform,
&gradient_brush,
(
gradient_brush.into(),
Some(to_backend_transform(gradient.transform)),
shape,
);
)
}
}
}
fn backend_gradient_alpha_space(space: PaintGradientAlphaSpace) -> InterpolationAlphaSpace {
match space {
PaintGradientAlphaSpace::Premultiplied => InterpolationAlphaSpace::Premultiplied,
PaintGradientAlphaSpace::Unpremultiplied => InterpolationAlphaSpace::Unpremultiplied,
}
}
fn to_backend_gradient_color_space(color_space: PaintGradientColorSpace) -> ColorSpaceTag {
match color_space {
PaintGradientColorSpace::Srgb => ColorSpaceTag::Srgb,
@@ -212,13 +212,13 @@ pub(crate) use context2d::{
canvas_context_begin_path_callback, canvas_context_bezier_curve_to_callback,
canvas_context_clear_rect_callback, canvas_context_close_path_callback,
canvas_context_create_image_data_callback, canvas_context_create_linear_gradient_callback,
canvas_context_draw_image_callback, canvas_context_ellipse_callback,
canvas_context_fill_callback, canvas_context_fill_rect_callback,
canvas_context_fill_style_getter_callback, canvas_context_fill_style_setter_callback,
canvas_context_fill_text_callback, canvas_context_font_getter_callback,
canvas_context_font_setter_callback, canvas_context_get_image_data_callback,
canvas_context_get_line_dash_callback, canvas_context_global_alpha_getter_callback,
canvas_context_global_alpha_setter_callback,
canvas_context_create_radial_gradient_callback, canvas_context_draw_image_callback,
canvas_context_ellipse_callback, canvas_context_fill_callback,
canvas_context_fill_rect_callback, canvas_context_fill_style_getter_callback,
canvas_context_fill_style_setter_callback, canvas_context_fill_text_callback,
canvas_context_font_getter_callback, canvas_context_font_setter_callback,
canvas_context_get_image_data_callback, canvas_context_get_line_dash_callback,
canvas_context_global_alpha_getter_callback, canvas_context_global_alpha_setter_callback,
canvas_context_global_composite_operation_getter_callback,
canvas_context_global_composite_operation_setter_callback,
canvas_context_image_smoothing_enabled_getter_callback,
@@ -24,8 +24,14 @@ use moli_layout::{
use moli_webapi_declare::WebApiObject;
use std::str::FromStr;
mod gradient;
mod text;
pub(crate) use gradient::{
canvas_context_create_linear_gradient_callback, canvas_context_create_radial_gradient_callback,
canvas_gradient_add_color_stop_callback,
};
const DEFAULT_IMAGE_SMOOTHING_QUALITY: &str = "low";
const CANVAS_CONTEXT_LINE_DASH_SLOT: &str = "__moliCanvasContextLineDash";
const TEXT_METRICS_WIDTH_SLOT: &str = "__moliTextMetricsWidth";
@@ -130,15 +136,6 @@ struct CanvasContextMeasureTextArgs {
text: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CanvasGradient.addColorStop")]
struct CanvasGradientAddColorStopArgs {
#[webidl(required)]
offset: f64,
#[webidl(required)]
_color: String,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CanvasRenderingContext2D.createImageData")]
struct CanvasContextCreateImageDataSizeArgs {
@@ -330,13 +327,11 @@ pub(crate) fn canvas_context_fill_style_getter_callback<'s>(
if !require_canvas_context_receiver(scope, args.this(), "fillStyle getter") {
return;
}
let value = context_string_slot(scope, args.this(), CANVAS_CONTEXT_FILL_STYLE_SLOT)
.unwrap_or_else(|| "#000000".to_owned());
if let Some(value) = v8_string(scope, &value) {
rv.set(value.into());
} else {
rv.set(v8::String::empty(scope).into());
}
rv.set(context_style_value(
scope,
args.this(),
CANVAS_CONTEXT_FILL_STYLE_SLOT,
));
}
pub(crate) fn canvas_context_fill_style_setter_callback<'s>(
@@ -347,6 +342,15 @@ pub(crate) fn canvas_context_fill_style_setter_callback<'s>(
if !require_canvas_context_receiver(scope, args.this(), "fillStyle setter") {
return;
}
if gradient::is_gradient(scope, args.get(0)) {
set_private_value(
scope,
args.this(),
CANVAS_CONTEXT_FILL_STYLE_SLOT,
args.get(0),
);
return;
}
let Some(raw) = canvas_context_dom_string_value(
scope,
args.get(0),
@@ -739,18 +743,52 @@ pub(crate) fn canvas_context_fill_rect_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
if !require_canvas_context_receiver(scope, args.this(), "fillRect") {
return;
}
let Some(canvas) = canvas_owner_from_context(scope, args.this()) else {
return;
};
let Some(rect) = normalized_rect(scope, &args, "CanvasRenderingContext2D.fillRect") else {
let Some(rect) = paint_rect_arguments(scope, &args, "CanvasRenderingContext2D.fillRect") else {
return;
};
let fill_style = context_string_slot(scope, args.this(), CANVAS_CONTEXT_FILL_STYLE_SLOT)
.unwrap_or_else(|| DEFAULT_FILL_STYLE.to_owned());
let color = fill_style_rgba(&fill_style);
let _ = with_canvas_like_pixels_mut(scope, canvas, |pixels, width, height| {
paint_rect(pixels, width, height, rect, color);
});
let state = canvas_path_state(scope, args.this());
let state = state.borrow();
if state.inverse_transform().is_none() {
return;
}
let fragment = PaintFragment::Fill {
shape: PaintShape::Rect(rect),
brush: context_style_brush(
scope,
args.this(),
CANVAS_CONTEXT_FILL_STYLE_SLOT,
PaintTransform2D::IDENTITY,
),
transform: state.transform(),
};
rasterize_canvas_fragment(scope, canvas, fragment);
}
fn paint_rect_arguments<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
prefix: &'static str,
) -> Option<moli_layout::PaintRect> {
let mut values = [0.0; 4];
for (index, value) in values.iter_mut().enumerate() {
*value = canvas_required_unrestricted_double_arg(scope, args, index as i32, prefix)?;
}
let [x, y, width, height] = values;
if !values.into_iter().all(f64::is_finite) || width == 0.0 || height == 0.0 {
return None;
}
Some(moli_layout::PaintRect::new(
x.min(x + width) as f32,
y.min(y + height) as f32,
width.abs() as f32,
height.abs() as f32,
))
}
pub(crate) fn canvas_context_clear_rect_callback<'s>(
@@ -1111,15 +1149,28 @@ pub(crate) fn canvas_context_fill_callback<'s>(
let Some(canvas) = canvas_owner_from_context(scope, args.this()) else {
return;
};
let brush = context_style_brush(
scope,
args.this(),
CANVAS_CONTEXT_FILL_STYLE_SLOT,
PaintTransform2D::IDENTITY,
);
let path_state = canvas_path_state(scope, args.this());
let fragment = with_path_state(&path_state, |state| {
if state.is_empty() || state.inverse_transform().is_none() {
return None;
}
let (path, transform) = if matches!(brush, PaintBrush::Solid(_)) {
(state.paint_path(), PaintTransform2D::IDENTITY)
} else {
// Paths capture their coordinates as they are constructed; the
// gradient uses the current user space at the time of painting.
(state.stroke_path()?, state.transform())
};
Some(PaintFragment::Fill {
shape: PaintShape::Path(state.paint_path()),
brush: PaintBrush::Solid(context_fill_color(scope, args.this())),
transform: PaintTransform2D::IDENTITY,
shape: PaintShape::Path(path),
brush,
transform,
})
});
if let Some(fragment) = fragment {
@@ -1380,13 +1431,11 @@ pub(crate) fn canvas_context_stroke_style_getter_callback<'s>(
if !require_canvas_context_receiver(scope, args.this(), "strokeStyle getter") {
return;
}
let value = context_string_slot(scope, args.this(), CANVAS_CONTEXT_STROKE_STYLE_SLOT)
.unwrap_or_else(|| DEFAULT_STROKE_STYLE.to_owned());
rv.set(
v8_string(scope, &value)
.unwrap_or_else(|| v8::String::empty(scope))
.into(),
);
rv.set(context_style_value(
scope,
args.this(),
CANVAS_CONTEXT_STROKE_STYLE_SLOT,
));
}
pub(crate) fn canvas_context_stroke_style_setter_callback<'s>(
@@ -1397,6 +1446,15 @@ pub(crate) fn canvas_context_stroke_style_setter_callback<'s>(
if !require_canvas_context_receiver(scope, args.this(), "strokeStyle setter") {
return;
}
if gradient::is_gradient(scope, args.get(0)) {
set_private_value(
scope,
args.this(),
CANVAS_CONTEXT_STROKE_STYLE_SLOT,
args.get(0),
);
return;
}
let Some(raw) = canvas_context_dom_string_value(
scope,
args.get(0),
@@ -1462,16 +1520,33 @@ fn with_path_state<T>(
update(&mut state.borrow_mut())
}
fn context_fill_color<'s>(
fn context_style_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
context: v8::Local<'s, v8::Object>,
) -> PaintColor {
let fill_style = context_string_slot(scope, context, CANVAS_CONTEXT_FILL_STYLE_SLOT)
.unwrap_or_else(|| DEFAULT_FILL_STYLE.to_owned());
color_with_global_alpha(
fill_style_rgba(&fill_style),
context_global_alpha(scope, context),
)
slot: &'static str,
) -> v8::Local<'s, v8::Value> {
get_private_value(scope, context, slot).unwrap_or_else(|| {
v8_string(scope, DEFAULT_FILL_STYLE)
.expect("default style")
.into()
})
}
fn context_style_brush<'s>(
scope: &mut v8::PinScope<'s, '_>,
context: v8::Local<'s, v8::Object>,
slot: &'static str,
transform: PaintTransform2D,
) -> PaintBrush {
let value = context_style_value(scope, context, slot);
let alpha = context_global_alpha(scope, context);
if let Some(brush) = gradient::brush(scope, value, alpha, transform) {
return brush;
}
let color = v8::Local::<v8::String>::try_from(value)
.map(|value| value.to_rust_string_lossy(scope))
.unwrap_or_else(|_| DEFAULT_FILL_STYLE.to_owned());
PaintBrush::Solid(color_with_global_alpha(fill_style_rgba(&color), alpha))
}
fn context_global_alpha<'s>(
@@ -1498,8 +1573,6 @@ fn context_stroke<'s>(
path: moli_layout::PaintPath,
transform: PaintTransform2D,
) -> PaintStroke {
let stroke_style = context_string_slot(scope, context, CANVAS_CONTEXT_STROKE_STYLE_SLOT)
.unwrap_or_else(|| DEFAULT_STROKE_STYLE.to_owned());
let join = match context_string_slot(scope, context, CANVAS_CONTEXT_LINE_JOIN_SLOT).as_deref() {
Some("round") => PaintLineJoin::Round,
Some("bevel") => PaintLineJoin::Bevel,
@@ -1512,9 +1585,11 @@ fn context_stroke<'s>(
};
PaintStroke {
path,
color: color_with_global_alpha(
fill_style_rgba(&stroke_style),
context_global_alpha(scope, context),
brush: context_style_brush(
scope,
context,
CANVAS_CONTEXT_STROKE_STYLE_SLOT,
PaintTransform2D::IDENTITY,
),
width: context_number_slot(scope, context, CANVAS_CONTEXT_LINE_WIDTH_SLOT)
.unwrap_or(DEFAULT_LINE_WIDTH) as f32,
@@ -1816,55 +1891,6 @@ pub(crate) fn canvas_context_get_line_dash_callback<'s>(
rv.set(v8::Array::new_with_elements(scope, &values).into());
}
pub(crate) fn canvas_context_create_linear_gradient_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let prefix = "CanvasRenderingContext2D.createLinearGradient";
let Some(x0) = canvas_required_unrestricted_double_arg(scope, &args, 0, prefix) else {
return;
};
let Some(y0) = canvas_required_unrestricted_double_arg(scope, &args, 1, prefix) else {
return;
};
let Some(x1) = canvas_required_unrestricted_double_arg(scope, &args, 2, prefix) else {
return;
};
let Some(y1) = canvas_required_unrestricted_double_arg(scope, &args, 3, prefix) else {
return;
};
if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
webidl::throw_dom_exception(
scope,
"NotSupportedError",
"Canvas gradient coordinates must be finite.",
);
return;
}
let gradient = v8::Object::new(scope);
if let Some(prototype) = global_constructor_prototype(scope, "CanvasGradient") {
let _ = gradient.set_prototype(scope, prototype.into());
}
rv.set(gradient.into());
}
pub(crate) fn canvas_gradient_add_color_stop_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<CanvasGradientAddColorStopArgs>(scope, &args) else {
return;
};
if !(0.0..=1.0).contains(&parsed.offset) {
webidl::throw_index_size_error(scope);
return;
}
rv.set_undefined();
}
pub(crate) fn canvas_context_measure_text_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -0,0 +1,232 @@
//! CanvasGradient owns private, GC-traced geometry and color stops. Neither
//! assignment nor painting coerces a gradient or reads author-visible properties.
use super::*;
use moli_layout::{
PaintGradientAlphaSpace, PaintGradientExtend, PaintGradientInterpolation, PaintGradientStop,
PaintLinearGradient, PaintPoint, PaintRadialGradient,
};
const COORDINATES_SLOT: &str = "__moliCanvasGradientCoordinates";
const STOPS_SLOT: &str = "__moliCanvasGradientStops";
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CanvasRenderingContext2D.createLinearGradient")]
struct LinearGradientArgs {
#[webidl(required, converter = "double")]
x0: f64,
#[webidl(required, converter = "double")]
y0: f64,
#[webidl(required, converter = "double")]
x1: f64,
#[webidl(required, converter = "double")]
y1: f64,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CanvasRenderingContext2D.createRadialGradient")]
struct RadialGradientArgs {
#[webidl(required, converter = "double")]
x0: f64,
#[webidl(required, converter = "double")]
y0: f64,
#[webidl(required, converter = "double")]
r0: f64,
#[webidl(required, converter = "double")]
x1: f64,
#[webidl(required, converter = "double")]
y1: f64,
#[webidl(required, converter = "double")]
r1: f64,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CanvasGradient.addColorStop")]
struct AddColorStopArgs {
#[webidl(required, converter = "double")]
offset: f64,
#[webidl(required)]
color: String,
}
pub(crate) fn canvas_context_create_linear_gradient_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !require_canvas_context_receiver(scope, args.this(), "createLinearGradient") {
return;
}
let Some(p) = webidl::parse_args::<LinearGradientArgs>(scope, &args) else {
return;
};
rv.set(create_gradient(scope, &[p.x0, p.y0, p.x1, p.y1]).into());
}
pub(crate) fn canvas_context_create_radial_gradient_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !require_canvas_context_receiver(scope, args.this(), "createRadialGradient") {
return;
}
let Some(p) = webidl::parse_args::<RadialGradientArgs>(scope, &args) else {
return;
};
if p.r0 < 0.0 || p.r1 < 0.0 {
webidl::throw_index_size_error(scope);
return;
}
rv.set(create_gradient(scope, &[p.x0, p.y0, p.r0, p.x1, p.y1, p.r1]).into());
}
fn create_gradient<'s>(
scope: &mut v8::PinScope<'s, '_>,
coordinates: &[f64],
) -> v8::Local<'s, v8::Object> {
let gradient = v8::Object::new(scope);
if let Some(prototype) = global_constructor_prototype(scope, "CanvasGradient") {
let _ = gradient.set_prototype(scope, prototype.into());
}
let coordinates = number_array(scope, coordinates);
let stops = v8::Array::new(scope, 0);
set_private_value(scope, gradient, COORDINATES_SLOT, coordinates.into());
set_private_value(scope, gradient, STOPS_SLOT, stops.into());
gradient
}
fn number_array<'s>(scope: &mut v8::PinScope<'s, '_>, values: &[f64]) -> v8::Local<'s, v8::Array> {
let values: Vec<v8::Local<'s, v8::Value>> = values
.iter()
.map(|value| v8::Number::new(scope, *value).into())
.collect();
v8::Array::new_with_elements(scope, &values)
}
fn private_array<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
slot: &'static str,
) -> Option<v8::Local<'s, v8::Array>> {
if value.is_proxy() {
return None;
}
let object = v8::Local::<v8::Object>::try_from(value).ok()?;
get_private_value(scope, object, slot)
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
}
pub(super) fn is_gradient<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> bool {
private_array(scope, value, COORDINATES_SLOT).is_some()
}
pub(crate) fn canvas_gradient_add_color_stop_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(stops) = private_array(scope, args.this().into(), STOPS_SLOT) else {
throw_type_error(scope, "CanvasGradient.addColorStop: Illegal invocation");
return;
};
let Some(p) = webidl::parse_args::<AddColorStopArgs>(scope, &args) else {
return;
};
if !(0.0..=1.0).contains(&p.offset) {
webidl::throw_index_size_error(scope);
return;
}
let Some(color) = canonical_canvas_fill_style(&p.color) else {
webidl::throw_dom_exception(
scope,
"SyntaxError",
"The provided value is not a valid color.",
);
return;
};
let [r, g, b, a] = fill_style_rgba(&color);
let stop = number_array(scope, &[p.offset, r.into(), g.into(), b.into(), a.into()]);
if let Some(key) = v8_string(scope, &stops.length().to_string()) {
// Define an own element: Array.prototype numeric setters are page code
// and must never observe mutation of this internal storage.
let _ = stops.create_data_property(scope, key.into(), stop.into());
}
}
fn numbers<const N: usize>(
scope: &mut v8::PinScope<'_, '_>,
array: v8::Local<'_, v8::Array>,
) -> Option<[f64; N]> {
let mut values = [0.0; N];
for (index, value) in values.iter_mut().enumerate() {
// These arrays never escape; every index is an own native Number.
*value = v8::Local::<v8::Number>::try_from(array.get_index(scope, index as u32)?)
.ok()?
.value();
}
Some(values)
}
pub(super) fn brush<'s>(
scope: &mut v8::PinScope<'s, '_>,
gradient: v8::Local<'s, v8::Value>,
alpha: f64,
transform: PaintTransform2D,
) -> Option<PaintBrush> {
let coordinates = private_array(scope, gradient, COORDINATES_SLOT)?;
let stored_stops = private_array(scope, gradient, STOPS_SLOT)?;
let mut stops = Vec::with_capacity(stored_stops.length() as usize);
for index in 0..stored_stops.length() {
let stop = v8::Local::<v8::Array>::try_from(stored_stops.get_index(scope, index)?).ok()?;
let [offset, r, g, b, a] = numbers(scope, stop)?;
stops.push(PaintGradientStop {
offset: offset as f32,
color: color_with_global_alpha([r as u8, g as u8, b as u8, a as u8], alpha),
});
}
// Stable ordering preserves the insertion order of coincident stops.
stops.sort_by(|a, b| a.offset.total_cmp(&b.offset));
if stops.is_empty() {
return Some(PaintBrush::Solid(PaintColor::TRANSPARENT));
}
let interpolation = PaintGradientInterpolation {
alpha_space: PaintGradientAlphaSpace::Unpremultiplied,
..Default::default()
};
let finite_float = |value: f64| value.clamp(-f64::from(f32::MAX), f64::from(f32::MAX)) as f32;
let point = |x, y| PaintPoint::new(finite_float(x), finite_float(y));
let brush = if coordinates.length() == 4 {
let [x0, y0, x1, y1] = numbers(scope, coordinates)?;
if x0 == x1 && y0 == y1 {
return Some(PaintBrush::Solid(PaintColor::TRANSPARENT));
}
PaintBrush::LinearGradient(PaintLinearGradient {
start: point(x0, y0),
end: point(x1, y1),
stops,
extend: PaintGradientExtend::Pad,
interpolation,
transform,
})
} else {
let [x0, y0, r0, x1, y1, r1] = numbers(scope, coordinates)?;
if x0 == x1 && y0 == y1 && r0 == r1 {
return Some(PaintBrush::Solid(PaintColor::TRANSPARENT));
}
PaintBrush::RadialGradient(PaintRadialGradient {
start_center: point(x0, y0),
start_radius: finite_float(r0),
end_center: point(x1, y1),
end_radius: finite_float(r1),
stops,
extend: PaintGradientExtend::Pad,
interpolation,
transform,
})
};
Some(brush)
}
@@ -234,32 +234,51 @@ pub(super) fn draw_text<'s>(
.max_width
.filter(|width| *width < shaped.width)
.map_or(1.0, |width| width / shaped.width);
let text_transform = PaintTransform2D::new([scale, 0.0, 0.0, 1.0, draw.x, draw.y]);
// Gradients are anchored in the canvas user space, not at the glyph origin.
let brush_transform =
PaintTransform2D::new([1.0 / scale, 0.0, 0.0, 1.0, -draw.x / scale, -draw.y]);
let transform = canvas_path_state(scope, context)
.borrow()
.transform()
.concatenate(PaintTransform2D::new([
scale, 0.0, 0.0, 1.0, draw.x, draw.y,
]));
.concatenate(text_transform);
let (fonts, fragments) = match draw.paint {
TextPaint::Fill => {
let color = context_fill_color(scope, context);
let mut fragments = Vec::with_capacity(shaped.runs.len());
for mut run in shaped.runs {
run.color = color;
run.transform = transform;
fragments.push(PaintFragment::GlyphRun(run));
}
(shaped.fonts, fragments)
}
TextPaint::Stroke => (
Vec::new(),
vec![PaintFragment::Stroke(context_stroke(
let brush = context_style_brush(
scope,
context,
shaped.outline_path(),
transform,
))],
),
CANVAS_CONTEXT_FILL_STYLE_SLOT,
brush_transform,
);
if let PaintBrush::Solid(color) = brush {
let mut fragments = Vec::with_capacity(shaped.runs.len());
for mut run in shaped.runs {
run.color = color;
run.transform = transform;
fragments.push(PaintFragment::GlyphRun(run));
}
(shaped.fonts, fragments)
} else {
(
Vec::new(),
vec![PaintFragment::Fill {
shape: PaintShape::Path(shaped.outline_path()),
brush,
transform,
}],
)
}
}
TextPaint::Stroke => {
let mut stroke = context_stroke(scope, context, shaped.outline_path(), transform);
stroke.brush = context_style_brush(
scope,
context,
CANVAS_CONTEXT_STROKE_STYLE_SLOT,
brush_transform,
);
(Vec::new(), vec![PaintFragment::Stroke(stroke)])
}
};
rasterize_canvas_scene(scope, canvas, |snapshot| {
snapshot.fonts = fonts;
@@ -3,13 +3,13 @@ use super::canvas::{
canvas_context_begin_path_callback, canvas_context_bezier_curve_to_callback,
canvas_context_clear_rect_callback, canvas_context_close_path_callback,
canvas_context_create_image_data_callback, canvas_context_create_linear_gradient_callback,
canvas_context_draw_image_callback, canvas_context_ellipse_callback,
canvas_context_fill_callback, canvas_context_fill_rect_callback,
canvas_context_fill_style_getter_callback, canvas_context_fill_style_setter_callback,
canvas_context_fill_text_callback, canvas_context_font_getter_callback,
canvas_context_font_setter_callback, canvas_context_get_image_data_callback,
canvas_context_get_line_dash_callback, canvas_context_global_alpha_getter_callback,
canvas_context_global_alpha_setter_callback,
canvas_context_create_radial_gradient_callback, canvas_context_draw_image_callback,
canvas_context_ellipse_callback, canvas_context_fill_callback,
canvas_context_fill_rect_callback, canvas_context_fill_style_getter_callback,
canvas_context_fill_style_setter_callback, canvas_context_fill_text_callback,
canvas_context_font_getter_callback, canvas_context_font_setter_callback,
canvas_context_get_image_data_callback, canvas_context_get_line_dash_callback,
canvas_context_global_alpha_getter_callback, canvas_context_global_alpha_setter_callback,
canvas_context_global_composite_operation_getter_callback,
canvas_context_global_composite_operation_setter_callback,
canvas_context_image_smoothing_enabled_getter_callback,
@@ -380,6 +380,13 @@ struct CanvasRenderingContext2dTemplateDeclaration {
)]
create_linear_gradient: (),
#[webapi(
method = "createRadialGradient",
length = 6,
callback = canvas_context_create_radial_gradient_callback
)]
create_radial_gradient: (),
#[webapi(
method = "createImageData",
length = 2,
@@ -0,0 +1,251 @@
use super::*;
fn gradient_pixels(script: &str, points: &[(u32, u32)], expected: &[[u8; 4]]) {
for constructor in [
"document.createElement('canvas')",
"new OffscreenCanvas(20,20)",
] {
let mut vm = new_storage_test_vm("https://canvas-gradients.test/");
let result = vm
.eval(&format!(
r#"(() => {{
const canvas = {constructor}; canvas.width = canvas.height = 20;
const ctx = canvas.getContext('2d');
{script}
return JSON.stringify({points}.map(([x,y]) => Array.from(ctx.getImageData(x,y,1,1).data)));
}})()"#,
points = serde_json::to_string(points).unwrap()
))
.expect("gradient should paint");
let actual: Vec<[u8; 4]> = serde_json::from_str(&result).unwrap();
assert_eq!(actual.len(), expected.len());
for ((actual, expected), point) in actual.iter().zip(expected).zip(points) {
for (actual, expected) in actual.iter().zip(expected) {
// Chromium's Skia rasterizer dithers gradient channels; allow
// two 8-bit levels, not differences in geometry or opacity.
assert!(
actual.abs_diff(*expected) <= 2,
"{constructor} pixel {point:?}: {actual} != {expected}; {script}"
);
}
}
}
}
#[test]
fn canvas_gradients_render_linear_and_radial_pixels() {
gradient_pixels(
"const g=ctx.createLinearGradient(0,0,10,0);g.addColorStop(0,'black');g.addColorStop(1,'white');ctx.fillStyle=g;ctx.fillRect(0,0,20,20);",
&[(0, 10), (2, 10), (7, 10), (12, 10)],
&[
[12, 12, 12, 255],
[63, 63, 63, 255],
[192, 192, 192, 255],
[255, 255, 255, 255],
],
);
gradient_pixels(
"const g=ctx.createRadialGradient(10,10,0,10,10,10);g.addColorStop(0,'red');g.addColorStop(1,'blue');ctx.fillStyle=g;ctx.fillRect(0,0,20,20);",
&[(0, 10), (7, 10), (12, 10), (19, 10)],
&[
[12, 0, 242, 255],
[190, 0, 65, 255],
[190, 0, 65, 255],
[13, 0, 243, 255],
],
);
}
#[test]
fn canvas_gradients_interpolate_straight_alpha_and_apply_global_alpha() {
gradient_pixels(
"const g=ctx.createLinearGradient(0,0,20,0);g.addColorStop(0,'red');g.addColorStop(1,'rgba(0,0,255,0)');ctx.fillStyle=g;ctx.fillRect(0,0,20,20);",
&[(7, 10), (12, 10)],
&[[160, 0, 96, 159], [96, 0, 157, 96]],
);
gradient_pixels(
"const g=ctx.createLinearGradient(0,0,20,0);g.addColorStop(0,'red');ctx.fillStyle=g;ctx.globalAlpha=0.5;ctx.fillRect(0,0,20,20);",
&[(7, 10)],
&[[255, 0, 0, 128]],
);
}
#[test]
fn canvas_gradients_preserve_duplicate_stop_order_and_empty_geometry() {
gradient_pixels(
"const g=ctx.createLinearGradient(0,0,20,0);g.addColorStop(1,'blue');g.addColorStop(0,'red');g.addColorStop(.5,'red');g.addColorStop(.5,'blue');ctx.fillStyle=g;ctx.fillRect(0,0,20,20);",
&[(7, 10), (12, 10)],
&[[255, 0, 0, 255], [0, 0, 255, 255]],
);
for constructor in [
"ctx.createLinearGradient(0,0,20,0)",
"ctx.createRadialGradient(10,10,0,10,10,10)",
] {
gradient_pixels(
&format!("ctx.fillStyle={constructor};ctx.fillRect(0,0,20,20);"),
&[(5, 5)],
&[[0; 4]],
);
}
for constructor in [
"ctx.createLinearGradient(0,0,0,0)",
"ctx.createRadialGradient(10,10,5,10,10,5)",
] {
gradient_pixels(
&format!(
"const g={constructor};g.addColorStop(0,'red');g.addColorStop(1,'blue');ctx.fillStyle=g;ctx.fillRect(0,0,20,20);"
),
&[(5, 5)],
&[[0; 4]],
);
}
}
#[test]
fn canvas_gradients_use_the_draw_time_transform_not_creation_transform() {
for (before, after) in [("ctx.scale(2,1);", ""), ("", "ctx.scale(2,1);")] {
gradient_pixels(
&format!(
"{before} const g=ctx.createLinearGradient(0,0,10,0);{after}g.addColorStop(0,'black');g.addColorStop(1,'white');ctx.fillStyle=g;ctx.fillRect(0,0,20,20);"
),
&[(2, 10), (7, 10), (12, 10), (17, 10)],
&[
[31, 31, 31, 255],
[96, 96, 96, 255],
[159, 159, 159, 255],
[224, 224, 224, 255],
],
);
}
// A path is already in canvas coordinates when the transform changes.
gradient_pixels(
"ctx.rect(0,0,20,20);ctx.scale(2,1);const g=ctx.createLinearGradient(0,0,10,0);g.addColorStop(0,'black');g.addColorStop(1,'white');ctx.fillStyle=g;ctx.fill();",
&[(2, 10), (17, 10)],
&[[31, 31, 31, 255], [224, 224, 224, 255]],
);
}
#[test]
fn canvas_gradients_paint_strokes() {
for draw in [
"ctx.strokeRect(0,4,20,12);",
"ctx.moveTo(0,10);ctx.lineTo(20,10);ctx.stroke();",
] {
gradient_pixels(
&format!(
"const g=ctx.createLinearGradient(0,0,20,0);g.addColorStop(0,'red');g.addColorStop(1,'blue');ctx.strokeStyle=g;ctx.lineWidth=16;{draw}"
),
&[(2, 10), (17, 10)],
&[[223, 0, 31, 255], [32, 0, 223, 255]],
);
}
}
#[test]
fn canvas_gradients_paint_text_in_canvas_not_glyph_coordinates() {
let mut vm = new_storage_test_vm("https://canvas-gradient-text.test/");
assert_eq!(
vm.eval(
r#"(() => {
for (const method of ['fillText','strokeText']) {
const ctx = new OffscreenCanvas(96,48).getContext('2d');
const g = ctx.createLinearGradient(0,0,10,0);
g.addColorStop(0,'red'); g.addColorStop(1,'blue');
ctx.fillStyle=ctx.strokeStyle=g; ctx.font='20px sans-serif';
ctx[method]('M',20,30);
const data=ctx.getImageData(0,0,96,48).data;
let count=0;
for (let i=0;i<data.length;i+=4) {
if (!data[i+3]) continue;
count++;
if (data[i]!==0 || data[i+1]!==0 || data[i+2]!==255) return method+' wrong brush origin';
}
if (!count) return method+' missing pixels';
}
return 'ok';
})()"#
)
.unwrap(),
"ok"
);
}
#[test]
fn canvas_gradients_share_live_stops_without_coercing_gradient_objects() {
let mut vm = new_storage_test_vm("https://canvas-gradient-state.test/");
assert_eq!(vm.eval(r#"(() => {
const canvas=document.createElement('canvas'); canvas.width=canvas.height=20;
const a=canvas.getContext('2d'), b=new OffscreenCanvas(20,20).getContext('2d');
const g=a.createLinearGradient(0,0,20,0);
g.toString=()=>{throw Error('must not stringify gradients')};
a.fillStyle=g; b.fillStyle=g;
let hits=0;
Object.defineProperty(Array.prototype,'0',{set(){hits++},configurable:true});
try { g.addColorStop(0,'red'); } finally { delete Array.prototype[0]; }
a.fillRect(0,0,20,20); b.fillRect(0,0,20,20);
const result=[a.fillStyle===g,b.fillStyle===g,hits,...a.getImageData(5,5,1,1).data,...b.getImageData(5,5,1,1).data];
canvas.width=20;
result.push(a.fillStyle,b.fillStyle===g);
return JSON.stringify(result);
})()"#).unwrap(), r##"[true,true,0,255,0,0,255,255,0,0,255,"#000000",true]"##);
}
#[test]
fn canvas_gradients_remain_live_when_only_the_context_retains_them() {
let mut vm = new_storage_test_vm("https://canvas-gradient-gc.test/");
vm.eval(
r#"globalThis.gradientContext = new OffscreenCanvas(20,20).getContext('2d');
(() => {
const g = gradientContext.createLinearGradient(0,0,20,0);
g.addColorStop(0,'red'); gradientContext.fillStyle = g;
})()"#,
)
.unwrap();
vm.renderer_document_isolate
.clone()
.with_entered_renderer_document_isolate(|isolate| {
isolate.low_memory_notification();
Ok(())
})
.expect("gradient storage should survive a collecting GC");
assert_eq!(
vm.eval(
r#"(() => {
const c=gradientContext;
c.fillStyle.addColorStop(1,'blue'); c.fillRect(0,0,20,20);
const left=c.getImageData(1,10,1,1).data, right=c.getImageData(18,10,1,1).data;
return left[0]>left[2] && right[2]>right[0] && left[3]===255 && right[3]===255;
})()"#
)
.unwrap(),
"true"
);
}
#[test]
fn canvas_gradients_validate_native_receivers_coordinates_radii_and_colors() {
let mut vm = new_storage_test_vm("https://canvas-gradient-validation.test/");
assert_eq!(
vm.eval(
r#"(() => {
const c=new OffscreenCanvas(20,20).getContext('2d');
const g=c.createLinearGradient(0,0,10,0);
const error=fn=>{try{fn();return 'ok'}catch(e){return e.name}};
return [c.createRadialGradient.length,
error(()=>c.createLinearGradient(0,0,Infinity,0)),
error(()=>c.createRadialGradient(0,0,0,0,0,Infinity)),
error(()=>c.createRadialGradient(0,0,-1,0,0,10)),
error(()=>g.addColorStop(0,'not-a-color')),
error(()=>g.addColorStop(NaN,'red')),
error(()=>g.addColorStop(2,'red')),
error(()=>g.addColorStop.call(Object.create(CanvasGradient.prototype),0,'red')),
error(()=>g.addColorStop.call(new Proxy(g,{}),0,'red')),
error(()=>c.createRadialGradient.call({},0,0,0,0,0,10)),
c.createRadialGradient(0,0,0,0,0,10) instanceof CanvasGradient
].join('|');
})()"#
)
.unwrap(),
"6|TypeError|TypeError|IndexSizeError|SyntaxError|TypeError|IndexSizeError|TypeError|TypeError|TypeError|true"
);
}
@@ -761,7 +761,7 @@ fn html_canvas_linear_gradient_surface_is_available_for_fingerprinting_scripts()
assert_eq!(
result,
r##"{"createType":"function","addType":"function","instance":true,"tag":"[object CanvasGradient]","addStops":"ok","badOffset":"IndexSizeError","badCoordinate":"NotSupportedError"}"##
r##"{"createType":"function","addType":"function","instance":true,"tag":"[object CanvasGradient]","addStops":"ok","badOffset":"IndexSizeError","badCoordinate":"TypeError"}"##
);
}
@@ -1319,7 +1319,7 @@ fn canvas_context_state_uses_private_slots_for_reflection_and_spoofing() {
assert_eq!(
result,
r##"{"initialOwnSlots":"","defaults":"#000000|10px sans-serif|true|low|1|source-over","beforeSpoof":"#ff0000|20px Arial|false|high|0.5|source-over","ownSlotsAfterSetter":"","beforeSpoofPixel":"255,0,0,255","afterSpoof":"#ff0000|20px Arial|false|high|0.5|source-over","afterSpoofPixel":"255,0,0,255","ownSlotsAfterSpoof":"__moliCanvasContextFillStyle,__moliCanvasContextFont,__moliCanvasContextGlobalAlpha,__moliCanvasContextGlobalCompositeOperation,__moliCanvasContextImageSmoothingEnabled,__moliCanvasContextImageSmoothingQuality","widthSame":true,"fakeFillStyleError":"TypeError","instance":true}"##
r##"{"initialOwnSlots":"","defaults":"#000000|10px sans-serif|true|low|1|source-over","beforeSpoof":"#ff0000|20px Arial|false|high|0.5|source-over","ownSlotsAfterSetter":"","beforeSpoofPixel":"255,0,0,128","afterSpoof":"#ff0000|20px Arial|false|high|0.5|source-over","afterSpoofPixel":"255,0,0,128","ownSlotsAfterSpoof":"__moliCanvasContextFillStyle,__moliCanvasContextFont,__moliCanvasContextGlobalAlpha,__moliCanvasContextGlobalCompositeOperation,__moliCanvasContextImageSmoothingEnabled,__moliCanvasContextImageSmoothingQuality","widthSame":true,"fakeFillStyleError":"TypeError","instance":true}"##
);
}
@@ -2134,6 +2134,7 @@ mod blob_response_headers;
mod browser_api;
mod cache_interfaces;
mod canvas_arguments;
mod canvas_gradients;
mod canvas_paths;
mod canvas_webgl;
mod child_dynamic_inline_scripts;