blitz-pdf 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -3
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/package.json +1 -1
- package/vendor/README.md +24 -0
- package/vendor/blitz-paint/Cargo.toml +50 -0
- package/vendor/blitz-paint/LICENSE-APACHE +176 -0
- package/vendor/blitz-paint/LICENSE-MIT +23 -0
- package/vendor/blitz-paint/src/color.rs +66 -0
- package/vendor/blitz-paint/src/debug_overlay.rs +168 -0
- package/vendor/blitz-paint/src/filters.rs +40 -0
- package/vendor/blitz-paint/src/gradient.rs +536 -0
- package/vendor/blitz-paint/src/kurbo_css/css_box.rs +847 -0
- package/vendor/blitz-paint/src/kurbo_css/mod.rs +77 -0
- package/vendor/blitz-paint/src/kurbo_css/non_uniform_radii.rs +60 -0
- package/vendor/blitz-paint/src/layers.rs +95 -0
- package/vendor/blitz-paint/src/lib.rs +190 -0
- package/vendor/blitz-paint/src/render/background.rs +968 -0
- package/vendor/blitz-paint/src/render/border.rs +631 -0
- package/vendor/blitz-paint/src/render/box_shadow.rs +157 -0
- package/vendor/blitz-paint/src/render/clip_path.rs +492 -0
- package/vendor/blitz-paint/src/render/form_controls.rs +103 -0
- package/vendor/blitz-paint/src/render/mask.rs +144 -0
- package/vendor/blitz-paint/src/render.rs +1351 -0
- package/vendor/blitz-paint/src/sizing.rs +63 -0
- package/vendor/blitz-paint/src/text.rs +747 -0
- package/wasm/blitz_pdf_bg.wasm +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
use super::ElementCx;
|
|
2
|
+
use crate::color::{Color, ToColorColor as _};
|
|
3
|
+
use anyrender::PaintScene;
|
|
4
|
+
use kurbo::{Rect, Vec2};
|
|
5
|
+
use peniko::{Compose, Fill, Mix};
|
|
6
|
+
|
|
7
|
+
impl ElementCx<'_, '_> {
|
|
8
|
+
pub(super) fn draw_outset_box_shadow(&self, scene: &mut impl PaintScene) {
|
|
9
|
+
let box_shadow = &self.style.get_effects().box_shadow.0;
|
|
10
|
+
let has_outset_shadow = box_shadow.iter().any(|s| !s.inset);
|
|
11
|
+
if !has_outset_shadow {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let current_color = self.style.clone_color();
|
|
16
|
+
let opacity = self.style.get_effects().opacity;
|
|
17
|
+
let bg_color = self
|
|
18
|
+
.style
|
|
19
|
+
.get_background()
|
|
20
|
+
.background_color
|
|
21
|
+
.resolve_to_absolute(¤t_color)
|
|
22
|
+
.as_srgb_color();
|
|
23
|
+
let bg_is_opaque = bg_color.components[3] >= 1.0;
|
|
24
|
+
let needs_clip = opacity < 1.0 || !bg_is_opaque;
|
|
25
|
+
|
|
26
|
+
let max_shadow_rect = box_shadow.iter().fold(Rect::ZERO, |prev, shadow| {
|
|
27
|
+
let x = shadow.base.horizontal.px() as f64 * self.scale;
|
|
28
|
+
let y = shadow.base.vertical.px() as f64 * self.scale;
|
|
29
|
+
let blur = shadow.base.blur.px() as f64 * self.scale;
|
|
30
|
+
let spread = shadow.spread.px() as f64 * self.scale;
|
|
31
|
+
let offset = spread + blur * 2.5;
|
|
32
|
+
|
|
33
|
+
let rect = self.frame.border_box.inflate(offset, offset) + Vec2::new(x, y);
|
|
34
|
+
|
|
35
|
+
prev.union(rect)
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
self.context.layer_manager.maybe_with_layer(
|
|
39
|
+
scene,
|
|
40
|
+
needs_clip,
|
|
41
|
+
1.0,
|
|
42
|
+
self.transform,
|
|
43
|
+
&self.frame.shadow_clip(max_shadow_rect),
|
|
44
|
+
None,
|
|
45
|
+
None,
|
|
46
|
+
|scene| {
|
|
47
|
+
for shadow in box_shadow.iter().filter(|s| !s.inset).rev() {
|
|
48
|
+
let shadow_color = shadow
|
|
49
|
+
.base
|
|
50
|
+
.color
|
|
51
|
+
.resolve_to_absolute(¤t_color)
|
|
52
|
+
.as_srgb_color();
|
|
53
|
+
|
|
54
|
+
let alpha = shadow_color.components[3];
|
|
55
|
+
if alpha != 0.0 {
|
|
56
|
+
let transform = self.transform.then_translate(Vec2 {
|
|
57
|
+
x: shadow.base.horizontal.px() as f64 * self.scale,
|
|
58
|
+
y: shadow.base.vertical.px() as f64 * self.scale,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let spread = shadow.spread.px() as f64 * self.scale;
|
|
62
|
+
let blur = shadow.base.blur.px() as f64;
|
|
63
|
+
|
|
64
|
+
// Without blur or spread the shadow is exactly the element's shape,
|
|
65
|
+
// so fill the border box path directly (preserving individual radii)
|
|
66
|
+
if blur == 0.0 && spread == 0.0 {
|
|
67
|
+
scene.fill(
|
|
68
|
+
Fill::NonZero,
|
|
69
|
+
transform,
|
|
70
|
+
shadow_color,
|
|
71
|
+
None,
|
|
72
|
+
&self.frame.border_box_path(),
|
|
73
|
+
);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// TODO draw shadows with matching individual radii instead of averaging
|
|
78
|
+
let radius = self.frame.border_radii.average();
|
|
79
|
+
let rect = self.frame.border_box.inflate(spread, spread);
|
|
80
|
+
|
|
81
|
+
// Fill the color
|
|
82
|
+
scene.draw_box_shadow(transform, rect, shadow_color, radius, blur);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(super) fn draw_inset_box_shadow(&self, scene: &mut impl PaintScene) {
|
|
90
|
+
let current_color = self.style.clone_color();
|
|
91
|
+
let box_shadow = &self.style.get_effects().box_shadow.0;
|
|
92
|
+
let has_inset_shadow = box_shadow.iter().any(|s| s.inset);
|
|
93
|
+
if !has_inset_shadow {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let padding_box = self.frame.padding_box_path();
|
|
98
|
+
|
|
99
|
+
for shadow in box_shadow.iter().filter(|s| s.inset) {
|
|
100
|
+
let shadow_color = shadow
|
|
101
|
+
.base
|
|
102
|
+
.color
|
|
103
|
+
.resolve_to_absolute(¤t_color)
|
|
104
|
+
.as_srgb_color();
|
|
105
|
+
if shadow_color == Color::TRANSPARENT {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// TODO draw shadows with matching individual radii instead of averaging
|
|
110
|
+
let radius = self.frame.border_radii.average();
|
|
111
|
+
let transform = self.transform.then_translate(Vec2 {
|
|
112
|
+
x: shadow.base.horizontal.px() as f64,
|
|
113
|
+
y: shadow.base.vertical.px() as f64,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
scene.push_layer(
|
|
117
|
+
Fill::NonZero,
|
|
118
|
+
Mix::Normal,
|
|
119
|
+
1.0,
|
|
120
|
+
self.transform,
|
|
121
|
+
&padding_box,
|
|
122
|
+
None,
|
|
123
|
+
None,
|
|
124
|
+
);
|
|
125
|
+
scene.fill(
|
|
126
|
+
Fill::NonZero,
|
|
127
|
+
self.transform,
|
|
128
|
+
shadow_color,
|
|
129
|
+
None,
|
|
130
|
+
&padding_box,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
scene.push_layer(
|
|
134
|
+
Fill::NonZero,
|
|
135
|
+
Compose::DestOut,
|
|
136
|
+
1.0,
|
|
137
|
+
self.transform,
|
|
138
|
+
&padding_box,
|
|
139
|
+
None,
|
|
140
|
+
None,
|
|
141
|
+
);
|
|
142
|
+
// The unshadowed area is the padding box shrunk by the spread radius
|
|
143
|
+
let spread = shadow.spread.px() as f64 * self.scale;
|
|
144
|
+
let hole = self.frame.padding_box.inflate(-spread, -spread);
|
|
145
|
+
scene.draw_box_shadow(
|
|
146
|
+
transform,
|
|
147
|
+
hole,
|
|
148
|
+
Color::WHITE,
|
|
149
|
+
radius,
|
|
150
|
+
shadow.base.blur.px() as f64 * self.scale,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
scene.pop_layer();
|
|
154
|
+
scene.pop_layer();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
use super::ElementCx;
|
|
2
|
+
use kurbo::{BezPath, Circle, Ellipse, Point, Rect, Shape, SvgArc, Vec2};
|
|
3
|
+
use style::values::computed::basic_shape::{BasicShape, ClipPath};
|
|
4
|
+
use style::values::computed::{Angle, CSSPixelLength, LengthPercentage};
|
|
5
|
+
use style::values::generics::basic_shape::{
|
|
6
|
+
ArcSize, ArcSweep, AxisEndPoint, AxisPosition, CommandEndPoint, ControlPoint, ControlReference,
|
|
7
|
+
GenericBasicShape, GenericPathOrShapeFunction, GenericShapeCommand, GenericShapeRadius,
|
|
8
|
+
ShapeBox, ShapeGeometryBox,
|
|
9
|
+
};
|
|
10
|
+
use style::values::generics::position::{GenericPosition, GenericPositionOrAuto};
|
|
11
|
+
|
|
12
|
+
impl ElementCx<'_, '_> {
|
|
13
|
+
/// Compute the rectangle (in scaled pixels, relative to the border box origin) described by
|
|
14
|
+
/// the CSS 2 `clip` property, if it applies. `clip` only applies to absolutely positioned
|
|
15
|
+
/// elements; `auto` components resolve to the corresponding border box edge.
|
|
16
|
+
pub(super) fn css_clip_rect(&self) -> Option<Rect> {
|
|
17
|
+
use style::computed_values::position::T as Position;
|
|
18
|
+
use style::values::computed::LengthOrAuto;
|
|
19
|
+
use style::values::generics::ClipRectOrAuto;
|
|
20
|
+
|
|
21
|
+
if !matches!(
|
|
22
|
+
self.style.get_box().position,
|
|
23
|
+
Position::Absolute | Position::Fixed
|
|
24
|
+
) {
|
|
25
|
+
return None;
|
|
26
|
+
}
|
|
27
|
+
let ClipRectOrAuto::Rect(rect) = &self.style.get_effects().clip else {
|
|
28
|
+
return None;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let border_box = self.frame.border_box;
|
|
32
|
+
let resolve = |component: &LengthOrAuto, auto: f64| match component {
|
|
33
|
+
LengthOrAuto::Auto => auto,
|
|
34
|
+
LengthOrAuto::LengthPercentage(length) => length.px() as f64 * self.scale,
|
|
35
|
+
};
|
|
36
|
+
let x0 = resolve(&rect.left, 0.0);
|
|
37
|
+
let y0 = resolve(&rect.top, 0.0);
|
|
38
|
+
let x1 = resolve(&rect.right, border_box.width());
|
|
39
|
+
let y1 = resolve(&rect.bottom, border_box.height());
|
|
40
|
+
|
|
41
|
+
Some(Rect::new(x0, y0, x1.max(x0), y1.max(y0)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Compute the clip-path BezPath (if any) for this element.
|
|
45
|
+
/// Returns `None` if clip-path is `none` or unsupported.
|
|
46
|
+
pub(super) fn clip_path_shape(&self) -> Option<BezPath> {
|
|
47
|
+
let clip_path = self.style.clone_clip_path();
|
|
48
|
+
match clip_path {
|
|
49
|
+
ClipPath::None => None,
|
|
50
|
+
ClipPath::Url(_) => {
|
|
51
|
+
// URL references (e.g. `clip-path: url(#myClip)`) are not yet supported
|
|
52
|
+
None
|
|
53
|
+
}
|
|
54
|
+
ClipPath::Shape(basic_shape, geometry_box) => {
|
|
55
|
+
let reference_box = self.resolve_geometry_box(&geometry_box);
|
|
56
|
+
self.basic_shape_to_path(&basic_shape, reference_box)
|
|
57
|
+
}
|
|
58
|
+
ClipPath::Box(geometry_box) => {
|
|
59
|
+
let reference_box = self.resolve_geometry_box(&geometry_box);
|
|
60
|
+
Some(Rect::from(reference_box).into_path(0.1))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Resolve a ShapeGeometryBox to a concrete rectangle (x, y, width, height) in scaled pixels
|
|
66
|
+
///
|
|
67
|
+
/// For SVG elements without associated CSS layout box, the used value for content-box and padding-box is fill-box and for border-box and margin-box is stroke-box.
|
|
68
|
+
/// For elements with associated CSS layout box, the used value for fill-box is content-box and for stroke-box and view-box is border-box.
|
|
69
|
+
fn resolve_geometry_box(&self, geometry_box: &ShapeGeometryBox) -> ReferenceBox {
|
|
70
|
+
match geometry_box {
|
|
71
|
+
ShapeGeometryBox::ElementDependent
|
|
72
|
+
| ShapeGeometryBox::StrokeBox
|
|
73
|
+
| ShapeGeometryBox::ViewBox
|
|
74
|
+
| ShapeGeometryBox::ShapeBox(ShapeBox::BorderBox) => ReferenceBox {
|
|
75
|
+
x: 0.0,
|
|
76
|
+
y: 0.0,
|
|
77
|
+
width: self.frame.border_box.width() / self.scale,
|
|
78
|
+
height: self.frame.border_box.height() / self.scale,
|
|
79
|
+
},
|
|
80
|
+
ShapeGeometryBox::ShapeBox(ShapeBox::PaddingBox) => ReferenceBox {
|
|
81
|
+
x: self.frame.border_width.x0 / self.scale,
|
|
82
|
+
y: self.frame.border_width.y0 / self.scale,
|
|
83
|
+
width: self.frame.padding_box.width() / self.scale,
|
|
84
|
+
height: self.frame.padding_box.height() / self.scale,
|
|
85
|
+
},
|
|
86
|
+
ShapeGeometryBox::FillBox | ShapeGeometryBox::ShapeBox(ShapeBox::ContentBox) => {
|
|
87
|
+
ReferenceBox {
|
|
88
|
+
x: (self.frame.border_width.x0 + self.frame.padding_width.x0) / self.scale,
|
|
89
|
+
y: (self.frame.border_width.y0 + self.frame.padding_width.y0) / self.scale,
|
|
90
|
+
width: self.frame.content_box.width() / self.scale,
|
|
91
|
+
height: self.frame.content_box.height() / self.scale,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
ShapeGeometryBox::ShapeBox(ShapeBox::MarginBox) => {
|
|
95
|
+
// Margin box is not tracked in CssBox, fall back to border box
|
|
96
|
+
ReferenceBox {
|
|
97
|
+
x: 0.0,
|
|
98
|
+
y: 0.0,
|
|
99
|
+
width: self.frame.border_box.width() / self.scale,
|
|
100
|
+
height: self.frame.border_box.height() / self.scale,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Convert a CSS basic-shape to a kurbo BezPath
|
|
107
|
+
fn basic_shape_to_path(
|
|
108
|
+
&self,
|
|
109
|
+
shape: &BasicShape,
|
|
110
|
+
reference_box: ReferenceBox,
|
|
111
|
+
) -> Option<BezPath> {
|
|
112
|
+
let w = reference_box.width;
|
|
113
|
+
let h = reference_box.height;
|
|
114
|
+
let ox = reference_box.x;
|
|
115
|
+
let oy = reference_box.y;
|
|
116
|
+
|
|
117
|
+
match shape {
|
|
118
|
+
GenericBasicShape::Circle(circle) => {
|
|
119
|
+
let (cx, cy) = resolve_position(&circle.position, w, h, ox, oy);
|
|
120
|
+
let r = resolve_shape_radius(&circle.radius, w, h, cx - ox, cy - oy);
|
|
121
|
+
Some(Circle::new(Point::new(cx, cy), r).into_path(0.1))
|
|
122
|
+
}
|
|
123
|
+
GenericBasicShape::Ellipse(ellipse) => {
|
|
124
|
+
let (cx, cy) = resolve_position(&ellipse.position, w, h, ox, oy);
|
|
125
|
+
let rx = resolve_shape_radius(&ellipse.semiaxis_x, w, h, cx - ox, cy - oy);
|
|
126
|
+
let ry = resolve_shape_radius(&ellipse.semiaxis_y, h, w, cy - oy, cx - ox);
|
|
127
|
+
Some(Ellipse::new(Point::new(cx, cy), (rx, ry), 0.0).into_path(0.1))
|
|
128
|
+
}
|
|
129
|
+
GenericBasicShape::Polygon(polygon) => {
|
|
130
|
+
let mut path = BezPath::new();
|
|
131
|
+
let _fill = &polygon.fill;
|
|
132
|
+
let coords = &polygon.coordinates;
|
|
133
|
+
|
|
134
|
+
if coords.is_empty() {
|
|
135
|
+
return None;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (i, coord) in coords.iter().enumerate() {
|
|
139
|
+
let x = ox + resolve_lp(&coord.0, w);
|
|
140
|
+
let y = oy + resolve_lp(&coord.1, h);
|
|
141
|
+
if i == 0 {
|
|
142
|
+
path.move_to(Point::new(x, y));
|
|
143
|
+
} else {
|
|
144
|
+
path.line_to(Point::new(x, y));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
path.close_path();
|
|
148
|
+
|
|
149
|
+
Some(path)
|
|
150
|
+
}
|
|
151
|
+
GenericBasicShape::Rect(inset_rect) => {
|
|
152
|
+
// inset() function: inset(top right bottom left round border-radius)
|
|
153
|
+
let top = resolve_lp(&inset_rect.rect.0, h);
|
|
154
|
+
let right = resolve_lp(&inset_rect.rect.1, w);
|
|
155
|
+
let bottom = resolve_lp(&inset_rect.rect.2, h);
|
|
156
|
+
let left = resolve_lp(&inset_rect.rect.3, w);
|
|
157
|
+
|
|
158
|
+
let x0 = ox + left;
|
|
159
|
+
let y0 = oy + top;
|
|
160
|
+
let x1 = ox + w - right;
|
|
161
|
+
let y1 = oy + h - bottom;
|
|
162
|
+
|
|
163
|
+
if x1 <= x0 || y1 <= y0 {
|
|
164
|
+
return None;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// TODO: Support border-radius on inset()
|
|
168
|
+
Some(kurbo::Rect::new(x0, y0, x1, y1).into_path(0.1))
|
|
169
|
+
}
|
|
170
|
+
GenericBasicShape::PathOrShape(path_or_shape) => match path_or_shape {
|
|
171
|
+
GenericPathOrShapeFunction::Path(path) => svg_path_to_bezpath(
|
|
172
|
+
path.commands(),
|
|
173
|
+
w,
|
|
174
|
+
h,
|
|
175
|
+
|v| *v as f64,
|
|
176
|
+
|v| *v as f64,
|
|
177
|
+
|v| *v as f64,
|
|
178
|
+
)
|
|
179
|
+
.map(|mut p| {
|
|
180
|
+
p.apply_affine(kurbo::Affine::translate((ox, oy)));
|
|
181
|
+
p
|
|
182
|
+
}),
|
|
183
|
+
GenericPathOrShapeFunction::Shape(shape) => svg_path_to_bezpath(
|
|
184
|
+
&shape.commands,
|
|
185
|
+
w,
|
|
186
|
+
h,
|
|
187
|
+
move |v: &LengthPercentage| {
|
|
188
|
+
v.resolve(CSSPixelLength::new(w as f32)).px() as f64
|
|
189
|
+
},
|
|
190
|
+
move |v: &LengthPercentage| {
|
|
191
|
+
v.resolve(CSSPixelLength::new(h as f32)).px() as f64
|
|
192
|
+
},
|
|
193
|
+
move |v: &Angle| v.degrees() as f64,
|
|
194
|
+
)
|
|
195
|
+
.map(|mut p| {
|
|
196
|
+
p.apply_affine(kurbo::Affine::translate((ox, oy)));
|
|
197
|
+
p
|
|
198
|
+
}),
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// A resolved reference box in scaled pixel coordinates
|
|
205
|
+
#[derive(Clone, Copy)]
|
|
206
|
+
struct ReferenceBox {
|
|
207
|
+
x: f64,
|
|
208
|
+
y: f64,
|
|
209
|
+
width: f64,
|
|
210
|
+
height: f64,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
impl From<ReferenceBox> for kurbo::Rect {
|
|
214
|
+
fn from(reference_box: ReferenceBox) -> Self {
|
|
215
|
+
kurbo::Rect::new(
|
|
216
|
+
reference_box.x,
|
|
217
|
+
reference_box.y,
|
|
218
|
+
reference_box.x + reference_box.width,
|
|
219
|
+
reference_box.y + reference_box.height,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// Resolve a LengthPercentage value against a basis length (already scaled)
|
|
225
|
+
fn resolve_lp(lp: &LengthPercentage, basis: f64) -> f64 {
|
|
226
|
+
let basis_css = CSSPixelLength::new(basis as f32);
|
|
227
|
+
lp.resolve(basis_css).px() as f64
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Resolve a position (or auto => center) to absolute coordinates
|
|
231
|
+
fn resolve_position(
|
|
232
|
+
position: &GenericPositionOrAuto<style::values::computed::Position>,
|
|
233
|
+
w: f64,
|
|
234
|
+
h: f64,
|
|
235
|
+
ox: f64,
|
|
236
|
+
oy: f64,
|
|
237
|
+
) -> (f64, f64) {
|
|
238
|
+
match position {
|
|
239
|
+
GenericPositionOrAuto::Auto => (ox + w / 2.0, oy + h / 2.0),
|
|
240
|
+
GenericPositionOrAuto::Position(pos) => {
|
|
241
|
+
let x = ox + resolve_lp(&pos.horizontal, w);
|
|
242
|
+
let y = oy + resolve_lp(&pos.vertical, h);
|
|
243
|
+
(x, y)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Resolve a shape radius keyword or length value
|
|
249
|
+
fn resolve_shape_radius(
|
|
250
|
+
radius: &GenericShapeRadius<LengthPercentage>,
|
|
251
|
+
primary_size: f64,
|
|
252
|
+
secondary_size: f64,
|
|
253
|
+
center_offset_primary: f64,
|
|
254
|
+
center_offset_secondary: f64,
|
|
255
|
+
) -> f64 {
|
|
256
|
+
match radius {
|
|
257
|
+
GenericShapeRadius::Length(lp) => resolve_lp(&lp.0, primary_size),
|
|
258
|
+
GenericShapeRadius::ClosestSide => center_offset_primary
|
|
259
|
+
.min(primary_size - center_offset_primary)
|
|
260
|
+
.min(center_offset_secondary)
|
|
261
|
+
.min(secondary_size - center_offset_secondary)
|
|
262
|
+
.max(0.0),
|
|
263
|
+
GenericShapeRadius::FarthestSide => center_offset_primary
|
|
264
|
+
.max(primary_size - center_offset_primary)
|
|
265
|
+
.max(center_offset_secondary)
|
|
266
|
+
.max(secondary_size - center_offset_secondary),
|
|
267
|
+
|
|
268
|
+
GenericShapeRadius::FarthestCorner => {
|
|
269
|
+
let dp = center_offset_primary.max(primary_size - center_offset_primary);
|
|
270
|
+
let ds = center_offset_secondary.max(secondary_size - center_offset_secondary);
|
|
271
|
+
dp.hypot(ds)
|
|
272
|
+
}
|
|
273
|
+
GenericShapeRadius::ClosestCorner => {
|
|
274
|
+
let dp = center_offset_primary
|
|
275
|
+
.min(primary_size - center_offset_primary)
|
|
276
|
+
.max(0.0);
|
|
277
|
+
let ds = center_offset_secondary
|
|
278
|
+
.min(secondary_size - center_offset_secondary)
|
|
279
|
+
.max(0.0);
|
|
280
|
+
dp.hypot(ds)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
type GenericPathCommand<Angle, N> = GenericShapeCommand<Angle, GenericPosition<N, N>, N>;
|
|
286
|
+
|
|
287
|
+
/// Convert an SVG path() to a kurbo BezPath.
|
|
288
|
+
/// The returned path is in the path's own coordinate system (origin at 0,0).
|
|
289
|
+
/// The caller is responsible for translating it to the reference box origin.
|
|
290
|
+
fn svg_path_to_bezpath<Angle: Copy, N>(
|
|
291
|
+
commands: &[GenericPathCommand<Angle, N>],
|
|
292
|
+
w: f64,
|
|
293
|
+
h: f64,
|
|
294
|
+
resolve_x: impl Fn(&N) -> f64,
|
|
295
|
+
resolve_y: impl Fn(&N) -> f64,
|
|
296
|
+
resolve_angle: impl Fn(&Angle) -> f64,
|
|
297
|
+
) -> Option<BezPath> {
|
|
298
|
+
if commands.is_empty() {
|
|
299
|
+
return None;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let mut bez = BezPath::new();
|
|
303
|
+
let mut cur = Point::ZERO;
|
|
304
|
+
let mut subpath_start = cur;
|
|
305
|
+
// Tracks the last control point for smooth continuation.
|
|
306
|
+
let mut last_control: Option<Point> = None;
|
|
307
|
+
|
|
308
|
+
for cmd in commands {
|
|
309
|
+
match &cmd {
|
|
310
|
+
GenericShapeCommand::Close => {
|
|
311
|
+
bez.close_path();
|
|
312
|
+
cur = subpath_start;
|
|
313
|
+
last_control = None;
|
|
314
|
+
}
|
|
315
|
+
GenericShapeCommand::Move { point } => {
|
|
316
|
+
let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
|
|
317
|
+
bez.move_to(p);
|
|
318
|
+
cur = p;
|
|
319
|
+
subpath_start = p;
|
|
320
|
+
last_control = None;
|
|
321
|
+
}
|
|
322
|
+
GenericShapeCommand::Line { point } => {
|
|
323
|
+
let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
|
|
324
|
+
bez.line_to(p);
|
|
325
|
+
cur = p;
|
|
326
|
+
last_control = None;
|
|
327
|
+
}
|
|
328
|
+
GenericShapeCommand::HLine { x } => {
|
|
329
|
+
cur.x = resolve_axis_endpoint(x, cur.x, w, &resolve_x);
|
|
330
|
+
bez.line_to(cur);
|
|
331
|
+
last_control = None;
|
|
332
|
+
}
|
|
333
|
+
GenericShapeCommand::VLine { y } => {
|
|
334
|
+
cur.y = resolve_axis_endpoint(y, cur.y, h, &resolve_y);
|
|
335
|
+
bez.line_to(cur);
|
|
336
|
+
last_control = None;
|
|
337
|
+
}
|
|
338
|
+
GenericShapeCommand::CubicCurve {
|
|
339
|
+
point,
|
|
340
|
+
control1,
|
|
341
|
+
control2,
|
|
342
|
+
} => {
|
|
343
|
+
let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
|
|
344
|
+
let c1 = resolve_control_point(control1, cur, p, &resolve_x, &resolve_y);
|
|
345
|
+
let c2 = resolve_control_point(control2, cur, p, &resolve_x, &resolve_y);
|
|
346
|
+
bez.curve_to(c1, c2, p);
|
|
347
|
+
last_control = Some(c2);
|
|
348
|
+
cur = p;
|
|
349
|
+
}
|
|
350
|
+
GenericShapeCommand::QuadCurve { point, control1 } => {
|
|
351
|
+
let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
|
|
352
|
+
let c1 = resolve_control_point(control1, cur, p, &resolve_x, &resolve_y);
|
|
353
|
+
bez.quad_to(c1, p);
|
|
354
|
+
last_control = Some(c1);
|
|
355
|
+
cur = p;
|
|
356
|
+
}
|
|
357
|
+
GenericShapeCommand::SmoothCubic { point, control2 } => {
|
|
358
|
+
let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
|
|
359
|
+
let c2 = resolve_control_point(control2, cur, p, &resolve_x, &resolve_y);
|
|
360
|
+
let c1 = reflect_point(last_control, cur);
|
|
361
|
+
bez.curve_to(c1, c2, p);
|
|
362
|
+
last_control = Some(c2);
|
|
363
|
+
cur = p;
|
|
364
|
+
}
|
|
365
|
+
GenericShapeCommand::SmoothQuad { point } => {
|
|
366
|
+
let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
|
|
367
|
+
let c1 = reflect_point(last_control, cur);
|
|
368
|
+
bez.quad_to(c1, p);
|
|
369
|
+
last_control = Some(c1);
|
|
370
|
+
cur = p;
|
|
371
|
+
}
|
|
372
|
+
GenericShapeCommand::Arc {
|
|
373
|
+
point,
|
|
374
|
+
radii,
|
|
375
|
+
arc_sweep,
|
|
376
|
+
arc_size,
|
|
377
|
+
rotate,
|
|
378
|
+
} => {
|
|
379
|
+
// SVG arc commands are complex; approximate as a line for now
|
|
380
|
+
let p = resolve_endpoint(point, cur, &resolve_x, &resolve_y);
|
|
381
|
+
let svg_arc = SvgArc {
|
|
382
|
+
from: cur,
|
|
383
|
+
to: p,
|
|
384
|
+
radii: Vec2 {
|
|
385
|
+
x: resolve_x(&radii.rx),
|
|
386
|
+
y: resolve_y(radii.ry.as_ref().unwrap_or(&radii.rx)),
|
|
387
|
+
},
|
|
388
|
+
x_rotation: resolve_angle(rotate),
|
|
389
|
+
large_arc: match arc_size {
|
|
390
|
+
ArcSize::Large => true,
|
|
391
|
+
ArcSize::Small => false,
|
|
392
|
+
},
|
|
393
|
+
// TODO: check mapping is correct
|
|
394
|
+
sweep: match arc_sweep {
|
|
395
|
+
ArcSweep::Ccw => true,
|
|
396
|
+
ArcSweep::Cw => false,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
let arc = kurbo::Arc::from_svg_arc(&svg_arc)?;
|
|
400
|
+
for el in arc.append_iter(0.1) {
|
|
401
|
+
bez.push(el);
|
|
402
|
+
}
|
|
403
|
+
last_control = None;
|
|
404
|
+
cur = p;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
Some(bez)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/// Resolve a CommandEndPoint to an absolute Point.
|
|
413
|
+
/// `ToPosition` is absolute; `ByCoordinate` is relative to `cur`.
|
|
414
|
+
fn resolve_endpoint<N>(
|
|
415
|
+
endpoint: &CommandEndPoint<GenericPosition<N, N>, N>,
|
|
416
|
+
cur: Point,
|
|
417
|
+
resolve_x: impl Fn(&N) -> f64,
|
|
418
|
+
resolve_y: impl Fn(&N) -> f64,
|
|
419
|
+
) -> Point {
|
|
420
|
+
match endpoint {
|
|
421
|
+
CommandEndPoint::ToPosition(pos) => {
|
|
422
|
+
Point::new(resolve_x(&pos.horizontal), resolve_y(&pos.vertical))
|
|
423
|
+
}
|
|
424
|
+
CommandEndPoint::ByCoordinate(coord) => {
|
|
425
|
+
Point::new(cur.x + resolve_x(&coord.x), cur.y + resolve_y(&coord.y))
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// Resolve a ControlPoint to an absolute Point.
|
|
431
|
+
/// - `Absolute`: uses the position directly.
|
|
432
|
+
/// - `Relative`: the coordinate pair is offset from a base point determined by `ControlReference`:
|
|
433
|
+
/// - `Start` (default): relative to the command's starting point (`cur`).
|
|
434
|
+
/// - `End`: relative to the command's end point (`end`).
|
|
435
|
+
/// - `Origin`: relative to the reference box origin (0,0 in path coordinates).
|
|
436
|
+
fn resolve_control_point<N>(
|
|
437
|
+
control_point: &ControlPoint<GenericPosition<N, N>, N>,
|
|
438
|
+
cur: Point,
|
|
439
|
+
end: Point,
|
|
440
|
+
resolve_x: impl Fn(&N) -> f64,
|
|
441
|
+
resolve_y: impl Fn(&N) -> f64,
|
|
442
|
+
) -> Point {
|
|
443
|
+
match control_point {
|
|
444
|
+
ControlPoint::Absolute(pos) => {
|
|
445
|
+
Point::new(resolve_x(&pos.horizontal), resolve_y(&pos.vertical))
|
|
446
|
+
}
|
|
447
|
+
ControlPoint::Relative(rel) => {
|
|
448
|
+
let base = match rel.reference {
|
|
449
|
+
ControlReference::Start => cur,
|
|
450
|
+
ControlReference::End => end,
|
|
451
|
+
ControlReference::Origin => Point::ZERO,
|
|
452
|
+
};
|
|
453
|
+
Point::new(
|
|
454
|
+
base.x + resolve_x(&rel.coord.x),
|
|
455
|
+
base.y + resolve_y(&rel.coord.y),
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/// Reflect a previous control point around the current point.
|
|
462
|
+
/// If there is no previous control point (previous command wasn't the matching curve type),
|
|
463
|
+
/// returns the current point itself.
|
|
464
|
+
fn reflect_point(last_control: Option<Point>, cur: Point) -> Point {
|
|
465
|
+
match last_control {
|
|
466
|
+
Some(ctrl) => Point::new(2.0 * cur.x - ctrl.x, 2.0 * cur.y - ctrl.y),
|
|
467
|
+
None => cur,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/// Resolve an AxisEndPoint to an absolute value.
|
|
472
|
+
/// `ToPosition` is absolute; `ByCoordinate` is relative to `cur_val`.
|
|
473
|
+
/// `w` and `h` are the reference box dimensions so keywords resolve against the correct axis.
|
|
474
|
+
fn resolve_axis_endpoint<N>(
|
|
475
|
+
endpoint: &AxisEndPoint<N>,
|
|
476
|
+
cur_val: f64,
|
|
477
|
+
basis: f64,
|
|
478
|
+
resolve: impl Fn(&N) -> f64,
|
|
479
|
+
) -> f64 {
|
|
480
|
+
use style::values::generics::basic_shape::AxisPositionKeyword;
|
|
481
|
+
match endpoint {
|
|
482
|
+
AxisEndPoint::ToPosition(AxisPosition::LengthPercent(lp)) => resolve(lp),
|
|
483
|
+
AxisEndPoint::ToPosition(AxisPosition::Keyword(kw)) => match kw {
|
|
484
|
+
AxisPositionKeyword::Left | AxisPositionKeyword::XStart => 0.0,
|
|
485
|
+
AxisPositionKeyword::Right | AxisPositionKeyword::XEnd => basis,
|
|
486
|
+
AxisPositionKeyword::Top | AxisPositionKeyword::YStart => 0.0,
|
|
487
|
+
AxisPositionKeyword::Bottom | AxisPositionKeyword::YEnd => basis,
|
|
488
|
+
AxisPositionKeyword::Center => basis / 2.0,
|
|
489
|
+
},
|
|
490
|
+
AxisEndPoint::ByCoordinate(val) => cur_val + resolve(val),
|
|
491
|
+
}
|
|
492
|
+
}
|