From 5f3f11bbe1f5d65291ffdaffc4b19a3f57a2dc1b Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 8 Sep 2026 16:21:09 +0800 Subject: [PATCH] fix(canvas): render native linear and radial gradients --- moli-layout/src/gradient.rs | 1 + moli-layout/src/gradient/interpolation.rs | 1 + moli-layout/src/lib.rs | 14 +- moli-layout/src/paint/form_controls.rs | 4 +- moli-layout/src/snapshot.rs | 14 +- moli-layout/tests/phase4_layout_contract.rs | 2 +- moli-paint/src/raster/mod.rs | 83 +++--- .../src/context_bootstrap/canvas.rs | 14 +- .../src/context_bootstrap/canvas/context2d.rs | 216 ++++++++------- .../canvas/context2d/gradient.rs | 232 ++++++++++++++++ .../canvas/context2d/text.rs | 57 ++-- .../context_bootstrap/media_file_template.rs | 21 +- .../src/script_vm/tests/canvas_gradients.rs | 251 ++++++++++++++++++ .../src/script_vm/tests/canvas_webgl.rs | 4 +- moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + 15 files changed, 743 insertions(+), 172 deletions(-) create mode 100644 moli-renderer-v8/src/context_bootstrap/canvas/context2d/gradient.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/canvas_gradients.rs diff --git a/moli-layout/src/gradient.rs b/moli-layout/src/gradient.rs index 68c6118ae9..c5738348f2 100644 --- a/moli-layout/src/gradient.rs +++ b/moli-layout/src/gradient.rs @@ -189,6 +189,7 @@ fn linear_gradient( stops, extend: gradient_extend(repeating), interpolation, + transform: LayoutTransform2D::IDENTITY, })) } diff --git a/moli-layout/src/gradient/interpolation.rs b/moli-layout/src/gradient/interpolation.rs index fd8a412c40..9ef60b9713 100644 --- a/moli-layout/src/gradient/interpolation.rs +++ b/moli-layout/src/gradient/interpolation.rs @@ -154,6 +154,7 @@ fn paint_interpolation(method: ColorInterpolationMethod) -> Option( }; 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( 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, diff --git a/moli-layout/src/snapshot.rs b/moli-layout/src/snapshot.rs index f4fc1f9500..4c5326b721 100644 --- a/moli-layout/src/snapshot.rs +++ b/moli-layout/src/snapshot.rs @@ -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, 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. diff --git a/moli-layout/tests/phase4_layout_contract.rs b/moli-layout/tests/phase4_layout_contract.rs index 728800a5a3..ad38e38ca0 100644 --- a/moli-layout/tests/phase4_layout_contract.rs +++ b/moli-layout/tests/phase4_layout_contract.rs @@ -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 diff --git a/moli-paint/src/raster/mod.rs b/moli-paint/src/raster/mod.rs index d86e91f200..eb9604d501 100644 --- a/moli-paint/src/raster/mod.rs +++ b/moli-paint/src/raster/mod.rs @@ -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) -> Option { 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) { 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, diff --git a/moli-renderer-v8/src/context_bootstrap/canvas.rs b/moli-renderer-v8/src/context_bootstrap/canvas.rs index 2105e55202..d5ee303eb0 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs b/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs index a2cb506524..f084bd99c2 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs @@ -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 { + 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( 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::::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::(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>, diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/context2d/gradient.rs b/moli-renderer-v8/src/context_bootstrap/canvas/context2d/gradient.rs new file mode 100644 index 0000000000..f5f6e8c86e --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/canvas/context2d/gradient.rs @@ -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::(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::(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> = 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> { + if value.is_proxy() { + return None; + } + let object = v8::Local::::try_from(value).ok()?; + get_private_value(scope, object, slot) + .and_then(|value| v8::Local::::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::(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( + 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::::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 { + 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::::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) +} diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/context2d/text.rs b/moli-renderer-v8/src/context_bootstrap/canvas/context2d/text.rs index d4b00042fa..1d9cdf45af 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/context2d/text.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/context2d/text.rs @@ -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; diff --git a/moli-renderer-v8/src/context_bootstrap/media_file_template.rs b/moli-renderer-v8/src/context_bootstrap/media_file_template.rs index b8c60b9c4a..cb469bb7e7 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_file_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_file_template.rs @@ -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, diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_gradients.rs b/moli-renderer-v8/src/script_vm/tests/canvas_gradients.rs new file mode 100644 index 0000000000..4918cf179d --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/canvas_gradients.rs @@ -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 { +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" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs index 049d1e4e25..e9d032bec5 100644 --- a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs +++ b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs @@ -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}"## ); } diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index d868020ce0..a6b5447c3b 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -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;