commit ee2f8cd405b387ff0dc9d67cc17f7511e01c990b parent 080ae42458a55154c890c14983456bee8f80e80f Author: Swarup Ukil <sukil@mozilla.com> Date: Thu, 6 Nov 2025 00:25:19 +0000 Bug 1921501 - Support parsing of <control-point> in shape(). r=boris,longsonr,firefox-style-system-reviewers,firefox-svg-reviewers,layout-reviewers Differential Revision: https://phabricator.services.mozilla.com/D270455 Diffstat:
19 files changed, 483 insertions(+), 208 deletions(-)
diff --git a/dom/svg/SVGAnimatedPathSegList.cpp b/dom/svg/SVGAnimatedPathSegList.cpp @@ -39,6 +39,16 @@ static StyleCommandEndPoint<float> MakeEndPoint(PositionType type, float x, } } +static StyleControlPoint<float> MakeControlPoint(PositionType type, float x, + float y) { + if (type == PositionType::Absolute) { + return StyleControlPoint<float>::Position({x, y}); + } else { + return StyleControlPoint<float>::Relative( + StyleRelativeControlPoint<float>{{x, y}, StyleControlReference::None}); + } +} + class MOZ_STACK_CLASS SVGPathSegmentInitWrapper final { public: explicit SVGPathSegmentInitWrapper(const SVGPathSegmentInit& aSVGPathSegment) @@ -87,24 +97,30 @@ class MOZ_STACK_CLASS SVGPathSegmentInitWrapper final { return StylePathCommand::CubicCurve( MakeEndPoint(PositionType::Absolute, mInit.mValues[4], mInit.mValues[5]), - {mInit.mValues[0], mInit.mValues[1]}, - {mInit.mValues[2], mInit.mValues[3]}); + MakeControlPoint(PositionType::Absolute, mInit.mValues[0], + mInit.mValues[1]), + MakeControlPoint(PositionType::Absolute, mInit.mValues[2], + mInit.mValues[3])); case 'c': return StylePathCommand::CubicCurve( MakeEndPoint(PositionType::Relative, mInit.mValues[4], mInit.mValues[5]), - {mInit.mValues[0], mInit.mValues[1]}, - {mInit.mValues[2], mInit.mValues[3]}); + MakeControlPoint(PositionType::Relative, mInit.mValues[0], + mInit.mValues[1]), + MakeControlPoint(PositionType::Relative, mInit.mValues[2], + mInit.mValues[3])); case 'Q': return StylePathCommand::QuadCurve( MakeEndPoint(PositionType::Absolute, mInit.mValues[2], mInit.mValues[3]), - {mInit.mValues[0], mInit.mValues[1]}); + MakeControlPoint(PositionType::Absolute, mInit.mValues[0], + mInit.mValues[1])); case 'q': return StylePathCommand::QuadCurve( MakeEndPoint(PositionType::Relative, mInit.mValues[2], mInit.mValues[3]), - {mInit.mValues[0], mInit.mValues[1]}); + MakeControlPoint(PositionType::Relative, mInit.mValues[0], + mInit.mValues[1])); case 'A': return StylePathCommand::Arc( MakeEndPoint(PositionType::Absolute, mInit.mValues[5], @@ -133,12 +149,14 @@ class MOZ_STACK_CLASS SVGPathSegmentInitWrapper final { return StylePathCommand::SmoothCubic( MakeEndPoint(PositionType::Absolute, mInit.mValues[2], mInit.mValues[3]), - {mInit.mValues[0], mInit.mValues[1]}); + MakeControlPoint(PositionType::Absolute, mInit.mValues[0], + mInit.mValues[1])); case 's': return StylePathCommand::SmoothCubic( MakeEndPoint(PositionType::Relative, mInit.mValues[2], mInit.mValues[3]), - {mInit.mValues[0], mInit.mValues[1]}); + MakeControlPoint(PositionType::Relative, mInit.mValues[0], + mInit.mValues[1])); case 'T': return StylePathCommand::SmoothQuad(MakeEndPoint( PositionType::Absolute, mInit.mValues[0], mInit.mValues[1])); diff --git a/dom/svg/SVGPathData.cpp b/dom/svg/SVGPathData.cpp @@ -245,6 +245,7 @@ static already_AddRefed<Path> BuildPathInternal( for (const auto& cmd : aPath) { seg = &cmd; + bool isRelative = false; switch (cmd.tag) { case Command::Tag::Close: // set this early to allow drawing of square caps for "M{x},{y} Z": @@ -271,15 +272,13 @@ static already_AddRefed<Path> BuildPathInternal( break; } case Command::Tag::CubicCurve: - cp1 = cmd.cubic_curve.control1.ToGfxPoint(aPercentageBasis); - cp2 = cmd.cubic_curve.control2.ToGfxPoint(aPercentageBasis); + isRelative = cmd.cubic_curve.point.IsByCoordinate(); segEnd = cmd.cubic_curve.point.ToGfxPoint(aPercentageBasis); - - if (cmd.cubic_curve.point.IsByCoordinate()) { - cp1 += segStart; - cp2 += segStart; - segEnd += segStart; - } + segEnd = isRelative ? segEnd + segStart : segEnd; + cp1 = cmd.cubic_curve.control1.ToGfxPoint(segStart, segEnd, isRelative, + aPercentageBasis); + cp2 = cmd.cubic_curve.control2.ToGfxPoint(segStart, segEnd, isRelative, + aPercentageBasis); if (segEnd != segStart || segEnd != cp1 || segEnd != cp2) { subpathHasLength = true; @@ -288,13 +287,12 @@ static already_AddRefed<Path> BuildPathInternal( break; case Command::Tag::QuadCurve: - cp1 = cmd.quad_curve.control1.ToGfxPoint(aPercentageBasis); + isRelative = cmd.quad_curve.point.IsByCoordinate(); segEnd = cmd.quad_curve.point.ToGfxPoint(aPercentageBasis); - - if (cmd.quad_curve.point.IsByCoordinate()) { - cp1 += segStart; - segEnd += segStart; // set before setting tcp2! - } + segEnd = isRelative ? segEnd + segStart + : segEnd; // set before setting tcp2! + cp1 = cmd.quad_curve.control1.ToGfxPoint(segStart, segEnd, isRelative, + aPercentageBasis); // Convert quadratic curve to cubic curve: tcp1 = segStart + (cp1 - segStart) * 2 / 3; @@ -359,14 +357,12 @@ static already_AddRefed<Path> BuildPathInternal( break; } case Command::Tag::SmoothCubic: - cp1 = prevSeg && prevSeg->IsCubicType() ? segStart * 2 - cp2 : segStart; - cp2 = cmd.smooth_cubic.control2.ToGfxPoint(aPercentageBasis); + isRelative = cmd.smooth_cubic.point.IsByCoordinate(); segEnd = cmd.smooth_cubic.point.ToGfxPoint(aPercentageBasis); - - if (cmd.smooth_cubic.point.IsByCoordinate()) { - cp2 += segStart; - segEnd += segStart; - } + segEnd = isRelative ? segEnd + segStart : segEnd; + cp1 = prevSeg && prevSeg->IsCubicType() ? segStart * 2 - cp2 : segStart; + cp2 = cmd.smooth_cubic.control2.ToGfxPoint(segStart, segEnd, isRelative, + aPercentageBasis); if (segEnd != segStart || segEnd != cp1 || segEnd != cp2) { subpathHasLength = true; @@ -546,6 +542,7 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, Point& segStart = prevSegEnd; Point segEnd; float segStartAngle, segEndAngle; + bool isRelative = false; switch (cmd.tag) // to find segStartAngle, segEnd and segEndAngle { @@ -570,15 +567,15 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, break; } case StylePathCommand::Tag::CubicCurve: { - Point cp1 = cmd.cubic_curve.control1.ToGfxPoint() * aZoom; - Point cp2 = cmd.cubic_curve.control2.ToGfxPoint() * aZoom; + isRelative = cmd.cubic_curve.point.IsByCoordinate(); segEnd = cmd.cubic_curve.point.ToGfxPoint() * aZoom; - - if (cmd.cubic_curve.point.IsByCoordinate()) { - cp1 += segStart; - cp2 += segStart; - segEnd += segStart; - } + segEnd = isRelative ? segEnd + segStart : segEnd; + Point cp1 = + cmd.cubic_curve.control1.ToGfxPoint(segStart, segEnd, isRelative) * + aZoom; + Point cp2 = + cmd.cubic_curve.control2.ToGfxPoint(segStart, segEnd, isRelative) * + aZoom; prevCP = cp2; segStartAngle = AngleOfVector( @@ -588,13 +585,13 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, break; } case StylePathCommand::Tag::QuadCurve: { - Point cp1 = cmd.quad_curve.control1.ToGfxPoint() * aZoom; + isRelative = cmd.quad_curve.point.IsByCoordinate(); segEnd = cmd.quad_curve.point.ToGfxPoint() * aZoom; - - if (cmd.quad_curve.point.IsByCoordinate()) { - cp1 += segStart; - segEnd += segStart; // set before setting tcp2! - } + segEnd = isRelative ? segEnd + segStart + : segEnd; // set before setting tcp2! + Point cp1 = + cmd.quad_curve.control1.ToGfxPoint(segStart, segEnd, isRelative) * + aZoom; prevCP = cp1; segStartAngle = AngleOfVector(cp1 == segStart ? segEnd : cp1, segStart); @@ -665,13 +662,12 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, const Point& cp1 = prevSeg && prevSeg->IsCubicType() ? segStart * 2 - prevCP : segStart; - Point cp2 = cmd.smooth_cubic.control2.ToGfxPoint() * aZoom; + isRelative = cmd.smooth_cubic.point.IsByCoordinate(); segEnd = cmd.smooth_cubic.point.ToGfxPoint() * aZoom; - - if (cmd.smooth_cubic.point.IsByCoordinate()) { - cp2 += segStart; - segEnd += segStart; - } + segEnd = isRelative ? segEnd + segStart : segEnd; + Point cp2 = + cmd.smooth_cubic.control2.ToGfxPoint(segStart, segEnd, isRelative) * + aZoom; prevCP = cp2; segStartAngle = AngleOfVector( diff --git a/dom/svg/SVGPathElement.cpp b/dom/svg/SVGPathElement.cpp @@ -139,8 +139,8 @@ static void CreatePathSegments(SVGPathElement* aPathElement, auto curve = StylePathCommand::CubicCurve( StyleCommandEndPoint<StyleCSSFloat>::ToPosition( {segEnd.x, segEnd.y}), - StyleCoordinatePair<StyleCSSFloat>{cp1.x, cp1.y}, - StyleCoordinatePair<StyleCSSFloat>{cp2.x, cp2.y}); + StyleControlPoint<StyleCSSFloat>::Position({cp1.x, cp1.y}), + StyleControlPoint<StyleCSSFloat>::Position({cp2.x, cp2.y})); aValues.AppendElement(new SVGPathSegment(aPathElement, curve)); } break; diff --git a/dom/svg/SVGPathSegUtils.cpp b/dom/svg/SVGPathSegUtils.cpp @@ -134,12 +134,10 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, ? aState.pos + aCommand.cubic_curve.point.ToGfxPoint() : aCommand.cubic_curve.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { - Point cp1 = aCommand.cubic_curve.control1.ToGfxPoint(); - Point cp2 = aCommand.cubic_curve.control2.ToGfxPoint(); - if (isRelative) { - cp1 += aState.pos; - cp2 += aState.pos; - } + Point cp1 = aCommand.cubic_curve.control1.ToGfxPoint(aState.pos, to, + isRelative); + Point cp2 = aCommand.cubic_curve.control2.ToGfxPoint(aState.pos, to, + isRelative); aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; @@ -154,9 +152,8 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, ? aState.pos + aCommand.quad_curve.point.ToGfxPoint() : aCommand.quad_curve.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { - Point cp = isRelative - ? aState.pos + aCommand.quad_curve.control1.ToGfxPoint() - : aCommand.quad_curve.control1.ToGfxPoint(); + Point cp = + aCommand.quad_curve.control1.ToGfxPoint(aState.pos, to, isRelative); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); aState.cp1 = cp; aState.cp2 = to; @@ -220,9 +217,8 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, : aCommand.smooth_cubic.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp1 = aState.pos - (aState.cp2 - aState.pos); - Point cp2 = isRelative ? aState.pos + - aCommand.smooth_cubic.control2.ToGfxPoint() - : aCommand.smooth_cubic.control2.ToGfxPoint(); + Point cp2 = aCommand.smooth_cubic.control2.ToGfxPoint(aState.pos, to, + isRelative); aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; diff --git a/dom/svg/SVGPathSegment.cpp b/dom/svg/SVGPathSegment.cpp @@ -20,7 +20,7 @@ NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SVGPathSegment, mSVGPathElement) //---------------------------------------------------------------------- // Implementation -void SVGPathSegment::AppendPoint( +void SVGPathSegment::AppendEndPoint( const StyleCommandEndPoint<StyleCSSFloat>& point) { if (point.IsToPosition()) { const auto& pos = point.AsToPosition(); @@ -33,6 +33,19 @@ void SVGPathSegment::AppendPoint( } } +void SVGPathSegment::AppendControlPoint( + const StyleControlPoint<StyleCSSFloat>& point) { + if (point.IsPosition()) { + const auto& pos = point.AsPosition(); + mValues.AppendElement(pos.horizontal); + mValues.AppendElement(pos.vertical); + } else if (point.IsRelative()) { + const auto& rel_point = point.AsRelative(); + mValues.AppendElement(rel_point.coord.x); + mValues.AppendElement(rel_point.coord.y); + } +} + SVGPathSegment::SVGPathSegment(SVGPathElement* aSVGPathElement, const StylePathCommand& aCommand) : mSVGPathElement(aSVGPathElement) { @@ -42,27 +55,24 @@ SVGPathSegment::SVGPathSegment(SVGPathElement* aSVGPathElement, break; case StylePathCommand::Tag::Move: mCommand.AssignLiteral(aCommand.move.point.IsToPosition() ? "M" : "m"); - AppendPoint(aCommand.move.point); + AppendEndPoint(aCommand.move.point); break; case StylePathCommand::Tag::Line: mCommand.AssignLiteral(aCommand.line.point.IsToPosition() ? "L" : "l"); - AppendPoint(aCommand.line.point); + AppendEndPoint(aCommand.line.point); break; case StylePathCommand::Tag::CubicCurve: mCommand.AssignLiteral(aCommand.cubic_curve.point.IsToPosition() ? "C" : "c"); - mValues.AppendElement(aCommand.cubic_curve.control1.x); - mValues.AppendElement(aCommand.cubic_curve.control1.y); - mValues.AppendElement(aCommand.cubic_curve.control2.x); - mValues.AppendElement(aCommand.cubic_curve.control2.y); - AppendPoint(aCommand.cubic_curve.point); + AppendControlPoint(aCommand.cubic_curve.control1); + AppendControlPoint(aCommand.cubic_curve.control2); + AppendEndPoint(aCommand.cubic_curve.point); break; case StylePathCommand::Tag::QuadCurve: mCommand.AssignLiteral(aCommand.quad_curve.point.IsToPosition() ? "Q" : "q"); - mValues.AppendElement(aCommand.quad_curve.control1.x); - mValues.AppendElement(aCommand.quad_curve.control1.y); - AppendPoint(aCommand.quad_curve.point); + AppendControlPoint(aCommand.quad_curve.control1); + AppendEndPoint(aCommand.quad_curve.point); break; case StylePathCommand::Tag::Arc: mCommand.AssignLiteral(aCommand.arc.point.IsToPosition() ? "A" : "a"); @@ -71,7 +81,7 @@ SVGPathSegment::SVGPathSegment(SVGPathElement* aSVGPathElement, mValues.AppendElement(aCommand.arc.rotate); mValues.AppendElement(aCommand.arc.arc_size == StyleArcSize::Large); mValues.AppendElement(aCommand.arc.arc_sweep == StyleArcSweep::Cw); - AppendPoint(aCommand.arc.point); + AppendEndPoint(aCommand.arc.point); break; case StylePathCommand::Tag::HLine: mCommand.AssignLiteral(aCommand.h_line.by_to == StyleByTo::To ? "H" @@ -86,14 +96,13 @@ SVGPathSegment::SVGPathSegment(SVGPathElement* aSVGPathElement, case StylePathCommand::Tag::SmoothCubic: mCommand.AssignLiteral(aCommand.smooth_cubic.point.IsToPosition() ? "S" : "s"); - mValues.AppendElement(aCommand.smooth_cubic.control2.x); - mValues.AppendElement(aCommand.smooth_cubic.control2.y); - AppendPoint(aCommand.smooth_cubic.point); + AppendControlPoint(aCommand.smooth_cubic.control2); + AppendEndPoint(aCommand.smooth_cubic.point); break; case StylePathCommand::Tag::SmoothQuad: mCommand.AssignLiteral(aCommand.smooth_quad.point.IsToPosition() ? "T" : "t"); - AppendPoint(aCommand.smooth_quad.point); + AppendEndPoint(aCommand.smooth_quad.point); break; } } diff --git a/dom/svg/SVGPathSegment.h b/dom/svg/SVGPathSegment.h @@ -42,7 +42,8 @@ class SVGPathSegment final : public nsWrapperCache { RefPtr<SVGPathElement> mSVGPathElement; nsString mCommand; nsTArray<float> mValues; - void AppendPoint(const StyleCommandEndPoint<StyleCSSFloat>& point); + void AppendEndPoint(const StyleCommandEndPoint<StyleCSSFloat>& point); + void AppendControlPoint(const StyleControlPoint<StyleCSSFloat>& point); }; } // namespace mozilla::dom diff --git a/layout/inspector/tests/test_bug877690.html b/layout/inspector/tests/test_bug877690.html @@ -293,7 +293,7 @@ function do_test() { // Regression test for bug 1255379. var shapeFunction = [ "close", "evenodd", "nonzero", "by", "to", "cw", "ccw", - "small", "large" ]; + "small", "large", "end", "origin", "start"]; var expected = [ "inherit", "initial", "unset", "revert", "revert-layer", "none", "url", "polygon", "circle", "ellipse", "inset", "path", "rect", "xywh", "fill-box", "stroke-box", diff --git a/layout/style/ServoStyleConstsInlines.h b/layout/style/ServoStyleConstsInlines.h @@ -1314,6 +1314,57 @@ inline gfx::Point StyleCommandEndPoint<LengthPercentage>::ToGfxPoint( } } +template <> +inline gfx::Point StyleControlPoint<StyleCSSFloat>::ToGfxPoint( + const gfx::Point aStatePos, const gfx::Point aEndPoint, + const bool isRelativeEndPoint, const CSSSize* aBasis) const { + if (IsPosition()) { + auto& pos = AsPosition(); + return pos.ToGfxPoint(); + } + + // Else + auto& point = AsRelative(); + auto cp = point.coord.ToGfxPoint(); + bool isRelativeDefaultCase = + point.reference == StyleControlReference::None && isRelativeEndPoint; + + if (point.reference == StyleControlReference::Start || + isRelativeDefaultCase) { + return cp + aStatePos; + } else if (point.reference == StyleControlReference::End) { + return cp + aEndPoint; + } else { + return cp; + } +} + +template <> +inline gfx::Point StyleControlPoint<LengthPercentage>::ToGfxPoint( + const gfx::Point aStatePos, const gfx::Point aEndPoint, + const bool isRelativeEndPoint, const CSSSize* aBasis) const { + MOZ_ASSERT(aBasis); + if (IsPosition()) { + auto& pos = AsPosition(); + return pos.ToGfxPoint(aBasis); + } + + // Else + auto& point = AsRelative(); + auto cp = point.coord.ToGfxPoint(aBasis); + bool isRelativeDefaultCase = + point.reference == StyleControlReference::None && isRelativeEndPoint; + + if (point.reference == StyleControlReference::Start || + isRelativeDefaultCase) { + return cp + aStatePos; + } else if (point.reference == StyleControlReference::End) { + return cp + aEndPoint; + } else { + return cp; + } +} + inline StylePhysicalSide ToStylePhysicalSide(mozilla::Side aSide) { // TODO(dshin): Should look into merging these two types... static_assert(static_cast<uint8_t>(mozilla::Side::eSideLeft) == diff --git a/servo/components/style/values/computed/basic_shape.rs b/servo/components/style/values/computed/basic_shape.rs @@ -58,6 +58,12 @@ pub type PathOrShapeFunction = generic::GenericPathOrShapeFunction<Angle, Length /// The computed value of `CoordinatePair`. pub type CoordinatePair = generic::CoordinatePair<LengthPercentage>; +/// The computed value of 'ControlPoint'. +pub type ControlPoint = generic::ControlPoint<LengthPercentage>; + +/// The computed value of 'RelativeControlPoint'. +pub type RelativeControlPoint = generic::RelativeControlPoint<LengthPercentage>; + /// The computed value of 'CommandEndPoint'. pub type CommandEndPoint = generic::CommandEndPoint<LengthPercentage>; @@ -229,12 +235,21 @@ impl From<&generic::CommandEndPoint<CSSFloat>> for CommandEndPoint { #[inline] fn from(p: &generic::CommandEndPoint<CSSFloat>) -> Self { match p { - generic::CommandEndPoint::<CSSFloat>::ToPosition(pos) => { - CommandEndPoint::ToPosition(pos.into()) - }, - generic::CommandEndPoint::<CSSFloat>::ByCoordinate(coord) => { - CommandEndPoint::ByCoordinate(coord.into()) - }, + generic::CommandEndPoint::ToPosition(pos) => Self::ToPosition(pos.into()), + generic::CommandEndPoint::ByCoordinate(coord) => Self::ByCoordinate(coord.into()), + } + } +} + +impl From<&generic::ControlPoint<CSSFloat>> for ControlPoint { + #[inline] + fn from(p: &generic::ControlPoint<CSSFloat>) -> Self { + match p { + generic::ControlPoint::Position(pos) => Self::Position(pos.into()), + generic::ControlPoint::Relative(point) => Self::Relative(RelativeControlPoint { + coord: CoordinatePair::from(&point.coord), + reference: point.reference, + }), } } } diff --git a/servo/components/style/values/generics/basic_shape.rs b/servo/components/style/values/generics/basic_shape.rs @@ -13,7 +13,6 @@ use crate::values::generics::rect::Rect; use crate::values::specified::svg_path::{PathCommand, SVGPathData}; use crate::Zero; use std::fmt::{self, Write}; -use std::ops::AddAssign; use style_traits::{CssWriter, ToCss}; /// TODO(bug 1982941): Replace with GenericPosition directly if ShapePosition is under utilized. @@ -691,6 +690,7 @@ impl<Angle, LengthPercentage> ToCss for Shape<Angle, LengthPercentage> where Angle: ToCss + Zero, LengthPercentage: PartialEq + ToCss, + ShapePosition<LengthPercentage>: ToCss, { fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where @@ -770,18 +770,18 @@ pub enum GenericShapeCommand<Angle, LengthPercentage> { /// The cubic Bézier curve command. CubicCurve { point: CommandEndPoint<LengthPercentage>, - control1: CoordinatePair<LengthPercentage>, - control2: CoordinatePair<LengthPercentage>, + control1: ControlPoint<LengthPercentage>, + control2: ControlPoint<LengthPercentage>, }, /// The quadratic Bézier curve command. QuadCurve { point: CommandEndPoint<LengthPercentage>, - control1: CoordinatePair<LengthPercentage>, + control1: ControlPoint<LengthPercentage>, }, /// The smooth command. SmoothCubic { point: CommandEndPoint<LengthPercentage>, - control2: CoordinatePair<LengthPercentage>, + control2: ControlPoint<LengthPercentage>, }, /// The smooth quadratic Bézier curve command. SmoothQuad { @@ -805,6 +805,7 @@ impl<Angle, LengthPercentage> ToCss for ShapeCommand<Angle, LengthPercentage> where Angle: ToCss + Zero, LengthPercentage: PartialEq + ToCss, + ShapePosition<LengthPercentage>: ToCss, { fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where @@ -840,11 +841,11 @@ where dest.write_str("curve ")?; point.to_css(dest)?; dest.write_str(" with ")?; - control1.to_css(dest)?; + control1.to_css(dest, point.is_abs())?; dest.write_char(' ')?; dest.write_char('/')?; dest.write_char(' ')?; - control2.to_css(dest) + control2.to_css(dest, point.is_abs()) }, QuadCurve { ref point, @@ -853,7 +854,7 @@ where dest.write_str("curve ")?; point.to_css(dest)?; dest.write_str(" with ")?; - control1.to_css(dest) + control1.to_css(dest, point.is_abs()) }, SmoothCubic { ref point, @@ -862,7 +863,7 @@ where dest.write_str("smooth ")?; point.to_css(dest)?; dest.write_str(" with ")?; - control2.to_css(dest) + control2.to_css(dest, point.is_abs()) }, SmoothQuad { ref point } => { dest.write_str("smooth ")?; @@ -1006,22 +1007,6 @@ impl<LengthPercentage: ToCss> ToCss for CommandEndPoint<LengthPercentage> { } } -impl<LengthPercentage: AddAssign> AddAssign<CoordinatePair<LengthPercentage>> - for CommandEndPoint<LengthPercentage> -{ - fn add_assign(&mut self, other: CoordinatePair<LengthPercentage>) { - match self { - CommandEndPoint::ToPosition(ref mut a) => { - a.horizontal += other.x; - a.vertical += other.y; - }, - CommandEndPoint::ByCoordinate(ref mut a) => { - *a += other; - }, - } - } -} - /// Defines a pair of coordinates, representing a rightward and downward offset, respectively, from /// a specified reference point. Percentages are resolved against the width or height, /// respectively, of the reference box. @@ -1060,6 +1045,137 @@ impl<LengthPercentage> CoordinatePair<LengthPercentage> { } } +/// Defines a control point for a quadratic or cubic Bézier curve, which can be specified +/// in absolute or relative coordinates. +/// https://drafts.csswg.org/css-shapes/#typedef-shape-control-point +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + Copy, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum ControlPoint<LengthPercentage> { + Position(ShapePosition<LengthPercentage>), + Relative(RelativeControlPoint<LengthPercentage>), +} + +impl<LengthPercentage> ControlPoint<LengthPercentage> { + /// Serialize <control-point> + pub fn to_css<W>(&self, dest: &mut CssWriter<W>, is_endpoint_abs: bool) -> fmt::Result + where + W: Write, + LengthPercentage: ToCss, + ShapePosition<LengthPercentage>: ToCss, + { + match self { + ControlPoint::Position(pos) => pos.to_css(dest), + ControlPoint::Relative(point) => point.to_css(dest, is_endpoint_abs), + } + } +} + +/// Defines a relative control point to a quadratic or cubic Bézier curve, dependent on the +/// reference value. The default "none" is to be relative to the command’s starting point. +/// https://drafts.csswg.org/css-shapes/#typedef-shape-relative-control-point +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct RelativeControlPoint<LengthPercentage> { + pub coord: CoordinatePair<LengthPercentage>, + pub reference: ControlReference, +} + +impl<LengthPercentage: ToCss> RelativeControlPoint<LengthPercentage> { + fn to_css<W>(&self, dest: &mut CssWriter<W>, is_endpoint_abs: bool) -> fmt::Result + where + W: Write, + { + self.coord.to_css(dest)?; + let should_omit_reference = match self.reference { + ControlReference::None => true, + ControlReference::Start => !is_endpoint_abs, + ControlReference::Origin => is_endpoint_abs, + ControlReference::End => false, + }; + if !should_omit_reference { + dest.write_str(" from ")?; + self.reference.to_css(dest)?; + } + Ok(()) + } +} + +impl<LengthPercentage: ComputeSquaredDistance> ComputeSquaredDistance + for RelativeControlPoint<LengthPercentage> +{ + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + self.coord.compute_squared_distance(&other.coord) + } +} + +/// Defines the point of reference for a <relative-control-point>. +/// +/// The default `None` is equivalent to `Origin` or `Start`, depending on +/// whether the associated <command-end-point> is absolutely or relatively +/// positioned, respectively. +/// https://drafts.csswg.org/css-shapes/#typedef-shape-relative-control-point +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + Copy, + Debug, + Deserialize, + Eq, + MallocSizeOf, + PartialEq, + Parse, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub enum ControlReference { + #[css(skip)] + None, + Start, + End, + Origin, +} + /// This indicates that the arc that is traced around the ellipse clockwise or counter-clockwise /// from the center. /// https://drafts.csswg.org/css-shapes-2/#typedef-shape-arc-sweep diff --git a/servo/components/style/values/specified/basic_shape.rs b/servo/components/style/values/specified/basic_shape.rs @@ -797,7 +797,7 @@ impl Parse for ShapeCommand { input: &mut Parser<'i, 't>, ) -> Result<Self, ParseError<'i>> { use crate::values::generics::basic_shape::{ - ArcSize, ArcSweep, ByTo, CommandEndPoint, CoordinatePair, + ArcSize, ArcSweep, ByTo, CommandEndPoint, ControlPoint, CoordinatePair, }; // <shape-command> = <move-command> | <line-command> | <hv-line-command> | @@ -841,9 +841,9 @@ impl Parse for ShapeCommand { let by_to = ByTo::parse(input)?; let point = CommandEndPoint::parse(context, input, by_to)?; input.expect_ident_matching("with")?; - let control1 = CoordinatePair::parse(context, input)?; + let control1 = ControlPoint::parse(context, input, by_to)?; if input.expect_delim('/').is_ok() { - let control2 = CoordinatePair::parse(context, input)?; + let control2 = ControlPoint::parse(context, input, by_to)?; Self::CubicCurve { point, control1, @@ -860,7 +860,7 @@ impl Parse for ShapeCommand { let by_to = ByTo::parse(input)?; let point = CommandEndPoint::parse(context, input, by_to)?; if input.try_parse(|i| i.expect_ident_matching("with")).is_ok() { - let control2 = CoordinatePair::parse(context, input)?; + let control2 = ControlPoint::parse(context, input, by_to)?; Self::SmoothCubic { point, control2, @@ -928,6 +928,34 @@ impl Parse for generic::CoordinatePair<LengthPercentage> { } } +impl generic::ControlPoint<LengthPercentage> { + /// Parse <control-point> = [ <position> | <relative-control-point> ] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + by_to: generic::ByTo, + ) -> Result<Self, ParseError<'i>> { + let coord = input.try_parse(|i| generic::CoordinatePair::parse(context, i)); + + // Parse <position> + if by_to.is_abs() && coord.is_err() { + let pos = parse_to_position(context, input)?; + return Ok(Self::Position(pos)); + } + + // Parse <relative-control-point> = <coordinate-pair> [from [ start | end | origin ]]? + let coord = coord?; + let mut reference = generic::ControlReference::None; + if input.try_parse(|i| i.expect_ident_matching("from")).is_ok() { + reference = generic::ControlReference::parse(input)?; + } + Ok(Self::Relative(generic::RelativeControlPoint { + coord, + reference, + })) + } +} + impl generic::CommandEndPoint<LengthPercentage> { /// Parse <command-end-point> = to <position> | by <coordinate-pair> pub fn parse<'i, 't>( @@ -937,10 +965,10 @@ impl generic::CommandEndPoint<LengthPercentage> { ) -> Result<Self, ParseError<'i>> { if by_to.is_abs() { let point = parse_to_position(context, input)?; - Ok(generic::CommandEndPoint::ToPosition(point)) + Ok(Self::ToPosition(point)) } else { let point = generic::CoordinatePair::parse(context, input)?; - Ok(generic::CommandEndPoint::ByCoordinate(point)) + Ok(Self::ByCoordinate(point)) } } } diff --git a/servo/components/style/values/specified/svg_path.rs b/servo/components/style/values/specified/svg_path.rs @@ -9,7 +9,8 @@ use crate::values::animated::{lists, Animate, Procedure}; use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; use crate::values::generics::basic_shape::GenericShapeCommand; use crate::values::generics::basic_shape::{ - ArcSize, ArcSweep, ByTo, CommandEndPoint, CoordinatePair, + ArcSize, ArcSweep, ByTo, CommandEndPoint, ControlPoint, ControlReference, CoordinatePair, + RelativeControlPoint, }; use crate::values::generics::position::GenericPosition as Position; use crate::values::CSSFloat; @@ -21,6 +22,9 @@ use std::slice; use style_traits::values::SequenceWriter; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +/// A specified value for `<position>`. +pub type ShapePosition = Position<CSSFloat, CSSFloat>; + /// Whether to allow empty string in the parser. #[derive(Clone, Debug, Eq, PartialEq)] #[allow(missing_docs)] @@ -222,10 +226,7 @@ impl PathCommand { Close }, Move { mut point } => { - if !point.is_abs() { - point += state.pos; - point = point.to_abs(); - } + point = point.to_abs(state.pos); state.pos = point.into(); state.subpath_start = point.into(); if reduce { @@ -234,10 +235,7 @@ impl PathCommand { Move { point } }, Line { mut point } => { - if !point.is_abs() { - point += state.pos; - point = point.to_abs(); - } + point = point.to_abs(state.pos); state.pos = point.into(); if reduce { state.last_command = *self; @@ -277,16 +275,13 @@ impl PathCommand { mut control1, mut control2, } => { - if !point.is_abs() { - point += state.pos; - point = point.to_abs(); - control1 += state.pos; - control2 += state.pos; - } + control1 = control1.to_abs(state.pos, point); + control2 = control2.to_abs(state.pos, point); + point = point.to_abs(state.pos); state.pos = point.into(); if reduce { state.last_command = *self; - state.last_control = control2; + state.last_control = control2.into(); } CubicCurve { point, @@ -298,21 +293,19 @@ impl PathCommand { mut point, mut control1, } => { - if !point.is_abs() { - point += state.pos; - point = point.to_abs(); - control1 += state.pos; - } + control1 = control1.to_abs(state.pos, point); + point = point.to_abs(state.pos); if reduce { - let c1 = state.pos + 2. * (control1 - state.pos) / 3.; - let control2 = CoordPair::from(point) + 2. * (control1 - point.into()) / 3.; + let c1 = state.pos + 2. * (CoordPair::from(control1) - state.pos) / 3.; + let control2 = CoordPair::from(point) + + 2. * (CoordPair::from(control1) - point.into()) / 3.; state.pos = point.into(); state.last_command = *self; - state.last_control = control1; + state.last_control = control1.into(); CubicCurve { point, - control1: c1, - control2, + control1: ControlPoint::Position(c1.into()), + control2: ControlPoint::Position(control2.into()), } } else { state.pos = point.into(); @@ -323,11 +316,8 @@ impl PathCommand { mut point, mut control2, } => { - if !point.is_abs() { - point += state.pos; - point = point.to_abs(); - control2 += state.pos; - } + control2 = control2.to_abs(state.pos, point); + point = point.to_abs(state.pos); if reduce { let control1 = match state.last_command { PathCommand::CubicCurve { @@ -342,11 +332,11 @@ impl PathCommand { _ => state.pos, }; state.pos = point.into(); - state.last_control = control2; + state.last_control = control2.into(); state.last_command = *self; CubicCurve { point, - control1, + control1: ControlPoint::Position(control1.into()), control2, } } else { @@ -355,10 +345,7 @@ impl PathCommand { } }, SmoothQuad { mut point } => { - if !point.is_abs() { - point += state.pos; - point = point.to_abs(); - } + point = point.to_abs(state.pos); if reduce { let control = match state.last_command { PathCommand::QuadCurve { @@ -377,8 +364,8 @@ impl PathCommand { state.last_control = control; CubicCurve { point, - control1, - control2, + control1: ControlPoint::Position(control1.into()), + control2: ControlPoint::Position(control2.into()), } } else { state.pos = point.into(); @@ -392,18 +379,16 @@ impl PathCommand { arc_size, rotate, } => { - if !point.is_abs() { - point += state.pos; - point = point.to_abs(); - } + point = point.to_abs(state.pos); state.pos = point.into(); if reduce { state.last_command = *self; if radii.x == 0. && radii.y == 0. { + let end_point = CoordPair::from(point); CubicCurve { point: CommandEndPoint::ToPosition(state.pos.into()), - control1: point.into(), - control2: point.into(), + control1: ControlPoint::Position(end_point.into()), + control2: ControlPoint::Position(end_point.into()), } } else { Arc { @@ -452,16 +437,16 @@ impl PathCommand { } => { dest.write_char(if point.is_abs() { 'C' } else { 'c' })?; dest.write_char(' ')?; - control1.to_css(dest)?; + control1.to_css(dest, point.is_abs())?; dest.write_char(' ')?; - control2.to_css(dest)?; + control2.to_css(dest, point.is_abs())?; dest.write_char(' ')?; CoordPair::from(point).to_css(dest) }, QuadCurve { point, control1 } => { dest.write_char(if point.is_abs() { 'Q' } else { 'q' })?; dest.write_char(' ')?; - control1.to_css(dest)?; + control1.to_css(dest, point.is_abs())?; dest.write_char(' ')?; CoordPair::from(point).to_css(dest) }, @@ -497,7 +482,7 @@ impl PathCommand { SmoothCubic { point, control2 } => { dest.write_char(if point.is_abs() { 'S' } else { 's' })?; dest.write_char(' ')?; - control2.to_css(dest)?; + control2.to_css(dest, point.is_abs())?; dest.write_char(' ')?; CoordPair::from(point).to_css(dest) }, @@ -567,14 +552,14 @@ impl ops::Div<CSSFloat> for CoordPair { impl CommandEndPoint<CSSFloat> { /// Converts <command-end-point> into absolutely positioned type. - pub fn to_abs(self) -> CommandEndPoint<CSSFloat> { + pub fn to_abs(self, state_pos: CoordPair) -> CommandEndPoint<CSSFloat> { // Consume self value. match self { CommandEndPoint::ToPosition(_) => self, CommandEndPoint::ByCoordinate(coord) => { let pos = Position { - horizontal: coord.x, - vertical: coord.y, + horizontal: coord.x + state_pos.x, + vertical: coord.y + state_pos.y, }; CommandEndPoint::ToPosition(pos) }, @@ -582,6 +567,44 @@ impl CommandEndPoint<CSSFloat> { } } +impl ControlPoint<CSSFloat> { + /// Converts <control-point> into absolutely positioned control point type. + pub fn to_abs( + self, + state_pos: CoordPair, + end_point: CommandEndPoint<CSSFloat>, + ) -> ControlPoint<CSSFloat> { + // Consume self value. + match self { + ControlPoint::Position(_) => self, + ControlPoint::Relative(point) => { + let mut pos = Position { + horizontal: point.coord.x, + vertical: point.coord.y, + }; + + match point.reference { + ControlReference::None if !end_point.is_abs() => { + pos.horizontal += state_pos.x; + pos.vertical += state_pos.y; + }, + ControlReference::Start => { + pos.horizontal += state_pos.x; + pos.vertical += state_pos.y; + }, + ControlReference::End => { + let end = CoordPair::from(end_point); + pos.horizontal += end.x; + pos.vertical += end.y; + }, + _ => (), + } + ControlPoint::Position(pos) + }, + } + } +} + impl From<CommandEndPoint<CSSFloat>> for CoordPair { #[inline] fn from(p: CommandEndPoint<CSSFloat>) -> Self { @@ -595,6 +618,24 @@ impl From<CommandEndPoint<CSSFloat>> for CoordPair { } } +impl From<ControlPoint<CSSFloat>> for CoordPair { + #[inline] + fn from(point: ControlPoint<CSSFloat>) -> Self { + match point { + ControlPoint::Position(pos) => CoordPair { + x: pos.horizontal, + y: pos.vertical, + }, + ControlPoint::Relative(_) => { + panic!( + "Attempted to convert a relative ControlPoint to CoordPair, which is lossy. \ + Consider converting it to absolute type first using `.to_abs()`." + ) + }, + } + } +} + impl From<CoordPair> for CommandEndPoint<CSSFloat> { #[inline] fn from(coord: CoordPair) -> Self { @@ -612,6 +653,17 @@ impl From<CoordPair> for Position<CSSFloat, CSSFloat> { } } +impl ToCss for ShapePosition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.horizontal.to_css(dest)?; + dest.write_char(' ')?; + self.vertical.to_css(dest) + } +} + /// SVG Path parser. struct PathParser<'a> { chars: Peekable<Cloned<slice::Iter<'a, u8>>>, @@ -752,11 +804,11 @@ impl<'a> PathParser<'a> { fn parse_curveto(&mut self, by_to: ByTo) -> Result<(), ()> { if by_to.is_abs() { parse_arguments!(self, CubicCurve, [ - control1 => parse_coord, control2 => parse_coord, point => parse_command_point_abs + control1 => parse_control_point, control2 => parse_control_point, point => parse_command_point_abs ]) } else { parse_arguments!(self, CubicCurve, [ - control1 => parse_coord, control2 => parse_coord, point => parse_command_point_rel + control1 => parse_control_point, control2 => parse_control_point, point => parse_command_point_rel ]) } } @@ -765,11 +817,11 @@ impl<'a> PathParser<'a> { fn parse_smooth_curveto(&mut self, by_to: ByTo) -> Result<(), ()> { if by_to.is_abs() { parse_arguments!(self, SmoothCubic, [ - control2 => parse_coord, point => parse_command_point_abs + control2 => parse_control_point, point => parse_command_point_abs ]) } else { parse_arguments!(self, SmoothCubic, [ - control2 => parse_coord, point => parse_command_point_rel + control2 => parse_control_point, point => parse_command_point_rel ]) } } @@ -778,11 +830,11 @@ impl<'a> PathParser<'a> { fn parse_quadratic_bezier_curveto(&mut self, by_to: ByTo) -> Result<(), ()> { if by_to.is_abs() { parse_arguments!(self, QuadCurve, [ - control1 => parse_coord, point => parse_command_point_abs + control1 => parse_control_point, point => parse_command_point_abs ]) } else { parse_arguments!(self, QuadCurve, [ - control1 => parse_coord, point => parse_command_point_rel + control1 => parse_control_point, point => parse_command_point_rel ]) } } @@ -853,6 +905,20 @@ fn parse_command_point_rel( Ok(CommandEndPoint::ByCoordinate(coord)) } +/// Parse a pair of values that describe the curve control point. +/// +/// Note: when the reference is None, the <control-point>'s reference +/// defaults to the commands coordinate mode (absolute or relative). +fn parse_control_point( + iter: &mut Peekable<Cloned<slice::Iter<u8>>>, +) -> Result<ControlPoint<f32>, ()> { + let coord = parse_coord(iter)?; + Ok(ControlPoint::Relative(RelativeControlPoint { + coord, + reference: ControlReference::None, + })) +} + /// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed /// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating /// point number. In other words, the logic here is similar with that of diff --git a/servo/ports/geckolib/cbindgen.toml b/servo/ports/geckolib/cbindgen.toml @@ -867,6 +867,17 @@ renaming_overrides_prefixing = true }; """ +"ControlPoint" = """ + inline gfx::Point ToGfxPoint( + const gfx::Point aStatePos, const gfx::Point aEndPoint, + const bool isRelativeEndPoint, const CSSSize* aBasis = nullptr) const; + gfx::Point ToGfxPoint( + const gfx::Point aStatePos, const gfx::Point aEndPoint, + const bool isRelativeEndPoint, const CSSSize& aBasis) const { + return ToGfxPoint(aStatePos, aEndPoint, isRelativeEndPoint, &aBasis); + } +""" + "TextOverflow" = """ StyleTextOverflow() : first(StyleTextOverflowSide::Clip()), diff --git a/testing/web-platform/meta/css/css-masking/clip-path/clip-path-shape-007.html.ini b/testing/web-platform/meta/css/css-masking/clip-path/clip-path-shape-007.html.ini @@ -1,2 +0,0 @@ -[clip-path-shape-007.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-masking/clip-path/clip-path-shape-008.html.ini b/testing/web-platform/meta/css/css-masking/clip-path/clip-path-shape-008.html.ini @@ -1,2 +0,0 @@ -[clip-path-shape-008.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-masking/clip-path/clip-path-shape-009.html.ini b/testing/web-platform/meta/css/css-masking/clip-path/clip-path-shape-009.html.ini @@ -1,2 +0,0 @@ -[clip-path-shape-009.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-masking/clip-path/clip-path-shape-010.html.ini b/testing/web-platform/meta/css/css-masking/clip-path/clip-path-shape-010.html.ini @@ -1,2 +0,0 @@ -[clip-path-shape-010.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-shapes/shape-functions/shape-function-computed.html.ini b/testing/web-platform/meta/css/css-shapes/shape-functions/shape-function-computed.html.ini @@ -1,12 +0,0 @@ -[shape-function-computed.html] - [Property clip-path value 'shape(from center, curve to center bottom with top right / bottom right)'] - expected: FAIL - - [Property clip-path value 'shape(from center, curve by 20px 20px with 10px 30px from end / 12px 32px from start)'] - expected: FAIL - - [Property clip-path value 'shape(from center right, curve by 20px 20px with 10px 30px from origin / 12px 32px from origin)'] - expected: FAIL - - [Property clip-path value 'shape(from 20px 40px, curve to top right with 10px 30px from origin / 12px 32px from origin)'] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-shapes/shape-functions/shape-function-valid.html.ini b/testing/web-platform/meta/css/css-shapes/shape-functions/shape-function-valid.html.ini @@ -35,15 +35,6 @@ [e.style['clip-path'\] = "shape(from 20px 40px, curve to right center with 10px 30px from origin / 12px 32px from start)" should set the property value] expected: FAIL - [e.style['clip-path'\] = "shape(from 20px 40px, curve by 20px 20px with 10px 30px from origin)" should set the property value] - expected: FAIL - - [e.style['clip-path'\] = "shape(from 20px 40px, curve by 20% 20em with 10px 30px from start / 12px 32px from end)" should set the property value] - expected: FAIL - - [e.style['clip-path'\] = "shape(from 20px 40px, curve by 20% 20em with 10.3% 30px from origin / 12pt 5.4% from start)" should set the property value] - expected: FAIL - [e.style['clip-path'\] = "shape(from top left, smooth to top right)" should set the property value] expected: FAIL @@ -53,9 +44,6 @@ [e.style['clip-path'\] = "shape(from right bottom, smooth by 20px 20px with 12px 32px)" should set the property value] expected: FAIL - [e.style['clip-path'\] = "shape(from 20px 40px, smooth by 20pt 20px with 12px 32px from start)" should set the property value] - expected: FAIL - [e.style['clip-path'\] = "shape(from center 40px, smooth by 20% 20% with 12px 32px from end)" should set the property value] expected: FAIL