servo/components/shared/canvas/canvas.rs
sagudev b799f27817
canvas: Use stored transform instead of querying canvas paint thread (#38097)
We already store transform in context state, so let's just use this when
querying instead of using IPC to ask canvas paint thread.

Testing: Existing WPT tests
work towards #38022

try run: https://github.com/sagudev/servo/actions/runs/16299182583

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
2025-07-16 14:59:10 +00:00

753 lines
22 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::default::Default;
use std::str::FromStr;
use euclid::Angle;
use euclid::approxeq::ApproxEq;
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
use ipc_channel::ipc::IpcSender;
use kurbo::{Affine, BezPath, ParamCurveNearest as _, PathEl, Point, Shape, Triangle};
use malloc_size_of_derive::MallocSizeOf;
use pixels::IpcSnapshot;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use style::color::AbsoluteColor;
use style::properties::style_structs::Font as FontStyleStruct;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Path(pub BezPath);
pub struct IndexSizeError;
impl Path {
pub fn new() -> Self {
Self(BezPath::new())
}
pub fn from_svg(s: &str) -> Self {
Self(BezPath::from_svg(s).unwrap_or_default())
}
pub fn transform(&mut self, transform: Transform2D<f64>) {
self.0.apply_affine(Affine::new(transform.to_array()));
}
/// <https://html.spec.whatwg.org/multipage/#ensure-there-is-a-subpath>
pub fn ensure_there_is_a_subpath(&mut self, x: f64, y: f64) {
// The user agent must check to see if the path has its need new subpath flag set.
if self.0.elements().is_empty() {
// If it does, then the user agent must create a new subpath with the point (x, y)
// as its first (and only) point,
// as if the moveTo() method had been called,
// and must then unset the path's need new subpath flag.
self.0.move_to((x, y));
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-closepath>
pub fn close_path(&mut self) {
// must do nothing if the object's path has no subpaths
if matches!(self.0.elements().last(), None | Some(PathEl::ClosePath)) {
return;
}
// Otherwise, it must mark the last subpath as closed,
// create a new subpath whose first point is the same as the previous subpath's first point,
// and finally add this new subpath to the path.
self.0.close_path();
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-moveto>
pub fn move_to(&mut self, x: f64, y: f64) {
// Step 1. If either of the arguments are infinite or NaN, then return.
if !(x.is_finite() && y.is_finite()) {
return;
}
// Step 2. Create a new subpath with the specified point as its first (and only) point.
self.0.move_to((x, y));
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-lineto>
pub fn line_to(&mut self, x: f64, y: f64) {
// Step 1. If either of the arguments are infinite or NaN, then return.
if !(x.is_finite() && y.is_finite()) {
return;
}
// Step 2. If the object's path has no subpaths, then ensure there is a subpath for (x, y).
self.ensure_there_is_a_subpath(x, y);
// Step 3. Otherwise, connect the last point in the subpath to the given point (x, y) using a straight line,
// and then add the given point (x, y) to the subpath.
self.0.line_to((x, y));
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-quadraticcurveto>
pub fn quadratic_curve_to(&mut self, cpx: f64, cpy: f64, x: f64, y: f64) {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(cpx.is_finite() && cpy.is_finite() && x.is_finite() && y.is_finite()) {
return;
}
// Step 2. Ensure there is a subpath for (cpx, cpy).
self.ensure_there_is_a_subpath(cpx, cpy);
// 3. Connect the last point in the subpath to the given point (x, y)
// using a quadratic Bézier curve with control point (cpx, cpy). [BEZIER]
// 4. Add the given point (x, y) to the subpath.
self.0.quad_to((cpx, cpy), (x, y));
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-beziercurveto>
pub fn bezier_curve_to(&mut self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(cp1x.is_finite() &&
cp1y.is_finite() &&
cp2x.is_finite() &&
cp2y.is_finite() &&
x.is_finite() &&
y.is_finite())
{
return;
}
// Step 2. Ensure there is a subpath for (cp1x, cp1y).
self.ensure_there_is_a_subpath(cp1x, cp1y);
// Step 3. Connect the last point in the subpath to the given point (x, y)
// using a cubic Bézier curve with control points (cp1x, cp1y) and (cp2x, cp2y). [BEZIER]
// Step 4. Add the point (x, y) to the subpath.
self.0.curve_to((cp1x, cp1y), (cp2x, cp2y), (x, y));
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arcto>
pub fn arc_to(
&mut self,
x1: f64,
y1: f64,
x2: f64,
y2: f64,
radius: f64,
) -> Result<(), IndexSizeError> {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(x1.is_finite() &&
y1.is_finite() &&
x2.is_finite() &&
y2.is_finite() &&
radius.is_finite())
{
return Ok(());
}
// Step 2. Ensure there is a subpath for (x1, y1).
self.ensure_there_is_a_subpath(x1, y1);
// Step 3. If either radius is negative, then throw an "IndexSizeError" DOMException.
if radius.is_sign_negative() {
return Err(IndexSizeError);
}
// Step 4. Let the point (x0, y0) be the last point in the subpath.
let Point { x: x0, y: y0 } = self.last_point().unwrap();
// Step 5. If the point (x0, y0) is equal to the point (x1, y1),
// or if the point (x1, y1) is equal to the point (x2, y2),
// or if radius is zero, then add the point (x1, y1) to the subpath,
// and connect that point to the previous point (x0, y0) by a straight line.
if ((x0, y0) == (x1, y1)) || ((x1, y1) == (x2, y2)) || radius.approx_eq(&0.0) {
self.0.line_to((x1, y1));
return Ok(());
}
// Step 6. Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2)
// all lie on a single straight line, then add the point (x1, y1) to the subpath,
// and connect that point to the previous point (x0, y0) by a straight line.
let direction = Triangle::from_coords((x0, y0), (x1, y1), (x2, y2)).area();
if direction == 0.0 {
self.0.line_to((x1, y1));
return Ok(());
}
// Step 7. Otherwise, let The Arc be the shortest arc given by circumference of the circle
// that has radius radius, and that has one point tangent to the half-infinite line
// that crosses the point (x0, y0) and ends at the point (x1, y1),
// and that has a different point tangent to the half-infinite line that ends at the point (x1, y1)
// and crosses the point (x2, y2).
// The points at which this circle touches these two lines are called the start
// and end tangent points respectively.
// Connect the point (x0, y0) to the start tangent point by a straight line,
// adding the start tangent point to the subpath,
// and then connect the start tangent point to the end tangent point by The Arc,
// adding the end tangent point to the subpath.
let a2 = (x0 - x1).powi(2) + (y0 - y1).powi(2);
let b2 = (x1 - x2).powi(2) + (y1 - y2).powi(2);
let d = {
let c2 = (x0 - x2).powi(2) + (y0 - y2).powi(2);
let cosx = (a2 + b2 - c2) / (2.0 * (a2 * b2).sqrt());
let sinx = (1.0 - cosx.powi(2)).sqrt();
radius / ((1.0 - cosx) / sinx)
};
// first tangent point
let anx = (x1 - x0) / a2.sqrt();
let any = (y1 - y0) / a2.sqrt();
let tp1 = Point2D::new(x1 - anx * d, y1 - any * d);
// second tangent point
let bnx = (x1 - x2) / b2.sqrt();
let bny = (y1 - y2) / b2.sqrt();
let tp2 = Point2D::new(x1 - bnx * d, y1 - bny * d);
// arc center and angles
let anticlockwise = direction < 0.0;
let cx = tp1.x + any * radius * if anticlockwise { 1.0 } else { -1.0 };
let cy = tp1.y - anx * radius * if anticlockwise { 1.0 } else { -1.0 };
let angle_start = (tp1.y - cy).atan2(tp1.x - cx);
let angle_end = (tp2.y - cy).atan2(tp2.x - cx);
self.0.line_to((tp1.x, tp2.x));
self.arc(cx, cy, radius, angle_start, angle_end, anticlockwise)
}
pub fn last_point(&mut self) -> Option<Point> {
// https://github.com/linebender/kurbo/pull/462
match self.0.elements().last()? {
PathEl::ClosePath => self
.0
.elements()
.iter()
.rev()
.skip(1)
.take_while(|el| !matches!(el, PathEl::ClosePath))
.last()
.and_then(|el| el.end_point()),
other => other.end_point(),
}
}
#[allow(clippy::too_many_arguments)]
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arc>
pub fn arc(
&mut self,
x: f64,
y: f64,
radius: f64,
start_angle: f64,
end_angle: f64,
counterclockwise: bool,
) -> Result<(), IndexSizeError> {
// ellipse() with both radii are equal and rotation is 0.
self.ellipse(
x,
y,
radius,
radius,
0.,
start_angle,
end_angle,
counterclockwise,
)
}
#[allow(clippy::too_many_arguments)]
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ellipse>
pub fn ellipse(
&mut self,
x: f64,
y: f64,
radius_x: f64,
radius_y: f64,
rotation_angle: f64,
start_angle: f64,
end_angle: f64,
counterclockwise: bool,
) -> Result<(), IndexSizeError> {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(x.is_finite() &&
y.is_finite() &&
radius_x.is_finite() &&
radius_y.is_finite() &&
rotation_angle.is_finite() &&
start_angle.is_finite() &&
end_angle.is_finite())
{
return Ok(());
}
// Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException.
if radius_x.is_sign_negative() || radius_y.is_sign_negative() {
return Err(IndexSizeError);
}
let mut start = Angle::radians(start_angle);
let mut end = Angle::radians(end_angle);
// Wrap angles mod 2 * PI if necessary
if !counterclockwise && start > end + Angle::two_pi() ||
counterclockwise && end > start + Angle::two_pi()
{
start = start.positive();
end = end.positive();
}
// Calculate the total arc we're going to sweep.
let sweep = match counterclockwise {
true => {
if end - start == Angle::two_pi() {
-Angle::two_pi()
} else if end > start {
-(Angle::two_pi() - (end - start))
} else {
-(start - end)
}
},
false => {
if start - end == Angle::two_pi() {
Angle::two_pi()
} else if start > end {
Angle::two_pi() - (start - end)
} else {
end - start
}
},
};
let arc = kurbo::Arc::new(
(x, y),
(radius_x, radius_y),
start.radians,
sweep.radians,
rotation_angle,
);
let mut iter = arc.path_elements(0.01);
let kurbo::PathEl::MoveTo(start_point) = iter.next().unwrap() else {
unreachable!()
};
self.line_to(start_point.x, start_point.y);
if sweep.radians.abs() > 1e-3 {
self.0.extend(iter);
}
Ok(())
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-rect>
pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()) {
return;
}
// Step 2. Create a new subpath containing just the four points
// (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order,
// with those four points connected by straight lines.
self.0.move_to((x, y));
self.0.line_to((x + w, y));
self.0.line_to((x + w, y + h));
self.0.line_to((x, y + h));
// Step 3. Mark the subpath as closed.
self.0.close_path();
// Step 4. Create a new subpath with the point (x, y) as the only point in the subpath.
self.0.move_to((x, y));
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath>
pub fn is_point_in_path(&self, x: f64, y: f64, fill_rule: FillRule) -> bool {
let p = Point::new(x, y);
// Step 1. If x or y are infinite or NaN, then return false.
if !p.is_finite() {
return false;
}
// Step 2. If the point given by the x and y coordinates,
// when treated as coordinates in the canvas coordinate space unaffected by the current transformation,
// is inside the intended path for path as determined by the fill rule indicated by fillRule,
// then return true.
// Open subpaths must be implicitly closed when computing the area inside the path,
// without affecting the actual subpaths.
let mut path = self.clone();
path.close_path();
let winding = path.0.winding(p);
let is_inside = match fill_rule {
FillRule::Nonzero => winding != 0,
FillRule::Evenodd => (winding % 2) != 0,
};
if is_inside {
return true;
}
// Points on the path itself must be considered to be inside the path.
path.0
.segments()
.any(|seg| seg.nearest(p, 0.00001).distance_sq < 0.00001)
}
pub fn bounding_box(&self) -> Rect<f64> {
let rect = self.0.control_box();
Rect::new(
Point2D::new(rect.origin().x, rect.origin().y),
Size2D::new(rect.width(), rect.height()),
)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum FillRule {
Nonzero,
Evenodd,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
pub struct CanvasId(pub u64);
#[derive(Debug, Deserialize, Serialize)]
pub enum CanvasMsg {
Canvas2d(Canvas2dMsg, CanvasId),
FromScript(FromScriptMsg, CanvasId),
Recreate(Option<Size2D<u64>>, CanvasId),
Close(CanvasId),
}
#[derive(Debug, Deserialize, Serialize)]
pub enum Canvas2dMsg {
Arc(Point2D<f32>, f32, f32, f32, bool),
ArcTo(Point2D<f32>, Point2D<f32>, f32),
DrawImage(IpcSnapshot, Rect<f64>, Rect<f64>, bool),
DrawEmptyImage(Size2D<u32>, Rect<f64>, Rect<f64>),
DrawImageInOther(CanvasId, Size2D<u32>, Rect<f64>, Rect<f64>, bool),
BeginPath,
BezierCurveTo(Point2D<f32>, Point2D<f32>, Point2D<f32>),
ClearRect(Rect<f32>),
Clip,
ClipPath(Path),
ClosePath,
Ellipse(Point2D<f32>, f32, f32, f32, f32, f32, bool),
Fill(FillOrStrokeStyle),
FillPath(FillOrStrokeStyle, Path),
FillText(String, f64, f64, Option<f64>, FillOrStrokeStyle, bool),
FillRect(Rect<f32>, FillOrStrokeStyle),
GetImageData(Rect<u32>, Size2D<u32>, IpcSender<IpcSnapshot>),
IsPointInCurrentPath(f64, f64, FillRule, IpcSender<bool>),
LineTo(Point2D<f32>),
MoveTo(Point2D<f32>),
MeasureText(String, IpcSender<TextMetrics>),
PutImageData(Rect<u32>, IpcSnapshot),
QuadraticCurveTo(Point2D<f32>, Point2D<f32>),
Rect(Rect<f32>),
RestoreContext,
SaveContext,
StrokeRect(Rect<f32>, FillOrStrokeStyle),
Stroke(FillOrStrokeStyle),
StrokePath(FillOrStrokeStyle, Path),
SetLineWidth(f32),
SetLineCap(LineCapStyle),
SetLineJoin(LineJoinStyle),
SetMiterLimit(f32),
SetLineDash(Vec<f32>),
SetLineDashOffset(f32),
SetGlobalAlpha(f32),
SetGlobalComposition(CompositionOrBlending),
SetTransform(Transform2D<f32>),
SetShadowOffsetX(f64),
SetShadowOffsetY(f64),
SetShadowBlur(f64),
SetShadowColor(AbsoluteColor),
SetFont(FontStyleStruct),
SetTextAlign(TextAlign),
SetTextBaseline(TextBaseline),
UpdateImage(IpcSender<()>),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum FromScriptMsg {
SendPixels(IpcSender<IpcSnapshot>),
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct CanvasGradientStop {
pub offset: f64,
pub color: AbsoluteColor,
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct LinearGradientStyle {
pub x0: f64,
pub y0: f64,
pub x1: f64,
pub y1: f64,
pub stops: Vec<CanvasGradientStop>,
}
impl LinearGradientStyle {
pub fn new(
x0: f64,
y0: f64,
x1: f64,
y1: f64,
stops: Vec<CanvasGradientStop>,
) -> LinearGradientStyle {
LinearGradientStyle {
x0,
y0,
x1,
y1,
stops,
}
}
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct RadialGradientStyle {
pub x0: f64,
pub y0: f64,
pub r0: f64,
pub x1: f64,
pub y1: f64,
pub r1: f64,
pub stops: Vec<CanvasGradientStop>,
}
impl RadialGradientStyle {
pub fn new(
x0: f64,
y0: f64,
r0: f64,
x1: f64,
y1: f64,
r1: f64,
stops: Vec<CanvasGradientStop>,
) -> RadialGradientStyle {
RadialGradientStyle {
x0,
y0,
r0,
x1,
y1,
r1,
stops,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SurfaceStyle {
pub surface_data: IpcSnapshot,
pub surface_size: Size2D<u32>,
pub repeat_x: bool,
pub repeat_y: bool,
pub transform: Transform2D<f32>,
}
impl SurfaceStyle {
pub fn new(
surface_data: IpcSnapshot,
surface_size: Size2D<u32>,
repeat_x: bool,
repeat_y: bool,
transform: Transform2D<f32>,
) -> Self {
Self {
surface_data,
surface_size,
repeat_x,
repeat_y,
transform,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum FillOrStrokeStyle {
Color(AbsoluteColor),
LinearGradient(LinearGradientStyle),
RadialGradient(RadialGradientStyle),
Surface(SurfaceStyle),
}
#[derive(
Clone, Copy, Debug, Display, Deserialize, EnumString, MallocSizeOf, PartialEq, Serialize,
)]
pub enum LineCapStyle {
Butt = 0,
Round = 1,
Square = 2,
}
#[derive(
Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
)]
pub enum LineJoinStyle {
Round = 0,
Bevel = 1,
Miter = 2,
}
#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, PartialEq, Serialize)]
#[strum(serialize_all = "kebab-case")]
pub enum RepetitionStyle {
Repeat,
RepeatX,
RepeatY,
NoRepeat,
}
/// <https://drafts.fxtf.org/compositing/#compositemode>
#[derive(
Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
)]
#[strum(serialize_all = "kebab-case")]
pub enum CompositionStyle {
Clear,
Copy,
SourceOver,
DestinationOver,
SourceIn,
DestinationIn,
SourceOut,
DestinationOut,
SourceAtop,
DestinationAtop,
Xor,
Lighter,
// PlusDarker,
// PlusLighter,
}
/// <https://drafts.fxtf.org/compositing/#ltblendmodegt>
#[derive(
Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
)]
#[strum(serialize_all = "kebab-case")]
pub enum BlendingStyle {
// Normal,
Multiply,
Screen,
Overlay,
Darken,
Lighten,
ColorDodge,
ColorBurn,
HardLight,
SoftLight,
Difference,
Exclusion,
Hue,
Saturation,
Color,
Luminosity,
}
#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
pub enum CompositionOrBlending {
Composition(CompositionStyle),
Blending(BlendingStyle),
}
impl Default for CompositionOrBlending {
fn default() -> CompositionOrBlending {
CompositionOrBlending::Composition(CompositionStyle::SourceOver)
}
}
impl FromStr for CompositionOrBlending {
type Err = ();
fn from_str(string: &str) -> Result<CompositionOrBlending, ()> {
if let Ok(op) = CompositionStyle::from_str(string) {
return Ok(CompositionOrBlending::Composition(op));
}
if let Ok(op) = BlendingStyle::from_str(string) {
return Ok(CompositionOrBlending::Blending(op));
}
Err(())
}
}
#[derive(
Clone,
Copy,
Debug,
Default,
Deserialize,
Display,
EnumString,
MallocSizeOf,
PartialEq,
Serialize,
)]
pub enum TextAlign {
#[default]
Start,
End,
Left,
Right,
Center,
}
#[derive(
Clone,
Copy,
Debug,
Default,
Deserialize,
Display,
EnumString,
MallocSizeOf,
PartialEq,
Serialize,
)]
pub enum TextBaseline {
Top,
Hanging,
Middle,
#[default]
Alphabetic,
Ideographic,
Bottom,
}
#[derive(
Clone,
Copy,
Debug,
Default,
Deserialize,
Display,
EnumString,
MallocSizeOf,
PartialEq,
Serialize,
)]
pub enum Direction {
Ltr,
Rtl,
#[default]
Inherit,
}
#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)]
pub struct TextMetrics {
pub width: f32,
pub actual_boundingbox_left: f32,
pub actual_boundingbox_right: f32,
pub actual_boundingbox_ascent: f32,
pub actual_boundingbox_descent: f32,
pub font_boundingbox_ascent: f32,
pub font_boundingbox_descent: f32,
pub em_height_ascent: f32,
pub em_height_descent: f32,
pub hanging_baseline: f32,
pub alphabetic_baseline: f32,
pub ideographic_baseline: f32,
}