mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
fix(canvas): render native linear and radial gradients
This commit is contained in:
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user