canvas: Fix transforming of gradient/bitmap strokes

This commit is contained in:
Mike Welsh 2022-07-17 14:46:01 -07:00
parent ef838a3536
commit 6f142c21fa
1 changed files with 316 additions and 189 deletions

View File

@ -35,6 +35,24 @@ struct ShapeData(Vec<CanvasDrawCommand>);
struct CanvasColor(String, u8, u8, u8, u8);
impl From<&Color> for CanvasColor {
fn from(color: &Color) -> CanvasColor {
CanvasColor(
format!(
"rgba({},{},{},{})",
color.r,
color.g,
color.b,
f32::from(color.a) / 255.0
),
color.r,
color.g,
color.b,
color.a,
)
}
}
impl CanvasColor {
/// Apply a color transformation to this color.
fn color_transform(&self, cxform: &ColorTransform) -> Self {
@ -54,7 +72,7 @@ enum CanvasDrawCommand {
Stroke {
path: Path2d,
line_width: f64,
stroke_style: CanvasFillStyle,
stroke_style: CanvasStrokeStyle,
line_cap: String,
line_join: String,
miter_limit: f64,
@ -68,17 +86,41 @@ enum CanvasDrawCommand {
},
}
/// Fill style for a canvas path.
enum CanvasFillStyle {
Color(CanvasColor),
Gradient(CanvasGradient),
TransformedGradient(TransformedGradient),
Pattern(CanvasPattern, bool),
Gradient(Gradient),
Bitmap(CanvasBitmap),
}
struct TransformedGradient {
struct Gradient {
gradient: CanvasGradient,
gradient_matrix: [f64; 6],
inverse_gradient_matrix: DomMatrix,
transform: Option<GradientTransform>,
}
/// A "complex" gradient transform, such as elliptical or skewed gradients.
///
/// Canvas does not provide an API for arbitrary gradient transforms, so we cheat by applying the
/// inverse of the gradient transform to the path, and then drawing the path with the gradient transform.
/// This results in an non-transformed shape with a transformed gradient.
struct GradientTransform {
matrix: [f64; 6],
inverse_matrix: DomMatrix,
}
/// Stroke style for a canvas path.
///
/// Gradients are handled differently for strokes vs. fills.
enum CanvasStrokeStyle {
Color(CanvasColor),
Gradient(swf::Gradient, Option<f64>),
Bitmap(CanvasBitmap),
}
struct CanvasBitmap {
pattern: CanvasPattern,
matrix: Matrix,
smoothed: bool,
}
#[allow(dead_code)]
@ -386,13 +428,26 @@ impl RenderBackend for WebCanvasRenderBackend {
MaskState::DrawContent => {
let mut line_scale = LineScales::new(&transform.matrix);
let dom_matrix = transform.matrix.to_dom_matrix();
self.set_transform(&transform.matrix);
let mut transform_dirty = true;
if let Some(shape) = self.shapes.get(shape.0) {
for command in shape.0.iter() {
match command {
CanvasDrawCommand::Fill { path, fill_style } => match fill_style {
CanvasDrawCommand::Fill { path, fill_style } => {
if transform_dirty {
let _ = self.context.set_transform(
transform.matrix.a.into(),
transform.matrix.b.into(),
transform.matrix.c.into(),
transform.matrix.d.into(),
transform.matrix.tx.to_pixels(),
transform.matrix.ty.to_pixels(),
);
transform_dirty = false;
}
match fill_style {
CanvasFillStyle::Color(color) => {
let color = color.color_transform(&transform.color_transform);
let color =
color.color_transform(&transform.color_transform);
self.context.set_fill_style(&color.0.into());
self.context.fill_with_path_2d_and_winding(
path,
@ -401,58 +456,48 @@ impl RenderBackend for WebCanvasRenderBackend {
}
CanvasFillStyle::Gradient(gradient) => {
self.set_color_filter(transform);
self.context.set_fill_style(gradient);
self.context.fill_with_path_2d_and_winding(
path,
CanvasWindingRule::Evenodd,
);
self.clear_color_filter();
}
CanvasFillStyle::TransformedGradient(gradient) => {
self.context.set_fill_style(&gradient.gradient);
if let Some(gradient_transform) = &gradient.transform {
// Canvas has no easy way to draw gradients with an arbitrary transform,
// but we can fake it by pushing the gradient's transform to the canvas,
// then transforming the path itself by the inverse.
self.set_color_filter(transform);
self.context.set_fill_style(&gradient.gradient);
let matrix = &gradient.gradient_matrix;
self.context
.transform(
matrix[0], matrix[1], matrix[2], matrix[3], matrix[4],
matrix[5],
)
.warn_on_error();
let matrix = &gradient_transform.matrix;
let _ = self.context.transform(
matrix[0], matrix[1], matrix[2], matrix[3],
matrix[4], matrix[5],
);
transform_dirty = true;
let untransformed_path = Path2d::new().unwrap();
untransformed_path.add_path_with_transformation(
path,
gradient.inverse_gradient_matrix.unchecked_ref(),
gradient_transform.inverse_matrix.unchecked_ref(),
);
self.context.fill_with_path_2d_and_winding(
&untransformed_path,
CanvasWindingRule::Evenodd,
);
self.context
.set_transform(
transform.matrix.a.into(),
transform.matrix.b.into(),
transform.matrix.c.into(),
transform.matrix.d.into(),
transform.matrix.tx.to_pixels(),
transform.matrix.ty.to_pixels(),
)
.unwrap();
} else {
self.context.fill_with_path_2d_and_winding(
path,
CanvasWindingRule::Evenodd,
);
}
self.clear_color_filter();
}
CanvasFillStyle::Pattern(patt, smoothed) => {
CanvasFillStyle::Bitmap(bitmap) => {
self.set_color_filter(transform);
self.context.set_image_smoothing_enabled(*smoothed);
self.context.set_fill_style(patt);
self.context.set_image_smoothing_enabled(bitmap.smoothed);
self.context.set_fill_style(&bitmap.pattern);
self.context.fill_with_path_2d_and_winding(
path,
CanvasWindingRule::Evenodd,
);
self.clear_color_filter();
}
},
}
}
CanvasDrawCommand::Stroke {
path,
line_width,
@ -466,6 +511,7 @@ impl RenderBackend for WebCanvasRenderBackend {
// Instead, reset the canvas transform, and apply the transform to the stroke path directly so
// that the geometry remains untransformed.
let _ = self.context.reset_transform();
transform_dirty = true;
let transformed_path = Path2d::new().unwrap();
transformed_path
.add_path_with_transformation(path, dom_matrix.unchecked_ref());
@ -478,41 +524,60 @@ impl RenderBackend for WebCanvasRenderBackend {
line_scale.transform_width(*line_width as f32, *scale_mode);
self.context.set_line_width(line_width.into());
match stroke_style {
CanvasFillStyle::Color(color) => {
CanvasStrokeStyle::Color(color) => {
let color =
color.color_transform(&transform.color_transform);
self.context.set_stroke_style(&color.0.into());
self.context.stroke_with_path(&transformed_path);
}
CanvasFillStyle::Gradient(gradient) => {
self.set_color_filter(transform);
self.context.set_stroke_style(gradient);
self.context.stroke_with_path(&transformed_path);
self.clear_color_filter();
}
CanvasFillStyle::TransformedGradient(gradient) => {
CanvasStrokeStyle::Gradient(gradient, focal_point) => {
// This is the hard case -- the Canvas API provides no good way to transform gradients,
// and the inverse-transform trick used above for gradient fills can't be used here
// because it will distort the stroke geometry.
// Another possibility is to avoid the Path2D API, instead drawing using explicit path
// commands (`context.lineTo`), then push the gradient transform, and finally stroke
// the path using `stroke()`. But this will be tons of JS calls if there are many strokes.
// So let's settle for allocating a new canvas gradient that is a best-effort match of the
// the desired transform. This will not match Flash exactly, but should be relatively rare.
let mut gradient = gradient.clone();
gradient.matrix = (transform.matrix
* Matrix::from(gradient.matrix))
.into();
let gradient = match focal_point {
Some(focal_point) => create_radial_gradient(
&self.context,
&gradient,
*focal_point,
false,
),
None => create_linear_gradient(
&self.context,
&gradient,
false,
),
};
if let Ok(gradient) = gradient {
self.set_color_filter(transform);
self.context.set_stroke_style(&gradient.gradient);
self.context.stroke_with_path(&transformed_path);
self.clear_color_filter();
}
CanvasFillStyle::Pattern(patt, smoothed) => {
self.context.set_image_smoothing_enabled(*smoothed);
self.context.set_stroke_style(patt);
}
CanvasStrokeStyle::Bitmap(bitmap) => {
// Set the CanvasPattern's matrix to the concatenated transform.
let bitmap_matrix = transform.matrix
* bitmap.matrix
* Matrix::scale(0.05, 0.05);
bitmap.pattern.set_transform(
bitmap_matrix.to_dom_matrix().unchecked_ref(),
);
self.set_color_filter(transform);
self.context.set_image_smoothing_enabled(bitmap.smoothed);
self.context.set_stroke_style(&bitmap.pattern);
self.context.stroke_with_path(&transformed_path);
self.clear_color_filter();
}
};
self.context
.set_transform(
transform.matrix.a.into(),
transform.matrix.b.into(),
transform.matrix.c.into(),
transform.matrix.d.into(),
transform.matrix.tx.to_pixels(),
transform.matrix.ty.to_pixels(),
)
.unwrap();
}
}
}
@ -700,85 +765,105 @@ fn swf_shape_to_canvas_commands(
bounds_viewbox_matrix.set_d(1.0 / 20.0);
for path in &shape.paths {
let (style, commands, is_fill, is_closed) = match &path {
match path {
DrawPath::Fill {
style, commands, ..
} => (*style, commands, true, false),
DrawPath::Stroke {
style,
commands,
is_closed,
} => (style.fill_style(), commands, false, *is_closed),
};
commands, style, ..
} => {
let canvas_path = Path2d::new().unwrap();
canvas_path.add_path_with_transformation(
&draw_commands_to_path2d(commands, false),
bounds_viewbox_matrix.unchecked_ref(),
);
let fill_style = match style {
FillStyle::Color(Color { r, g, b, a }) => CanvasFillStyle::Color(CanvasColor(
format!("rgba({},{},{},{})", r, g, b, f32::from(*a) / 255.0),
*r,
*g,
*b,
*a,
)),
FillStyle::LinearGradient(gradient) => {
create_linear_gradient(context, gradient, is_fill).unwrap()
}
FillStyle::RadialGradient(gradient) => {
create_radial_gradient(context, gradient, 0.0, is_fill).unwrap()
}
FillStyle::Color(color) => CanvasFillStyle::Color(color.into()),
FillStyle::LinearGradient(gradient) => CanvasFillStyle::Gradient(
create_linear_gradient(context, gradient, true).unwrap(),
),
FillStyle::RadialGradient(gradient) => CanvasFillStyle::Gradient(
create_radial_gradient(context, gradient, 0.0, true).unwrap(),
),
FillStyle::FocalGradient {
gradient,
focal_point,
} => create_radial_gradient(context, gradient, focal_point.to_f64(), is_fill).unwrap(),
} => CanvasFillStyle::Gradient(
create_radial_gradient(context, gradient, focal_point.to_f64(), true)
.unwrap(),
),
FillStyle::Bitmap {
id,
matrix,
is_smoothed,
is_repeating,
} => {
if let Some(bitmap) = bitmap_source
.bitmap(*id)
.and_then(|bitmap| bitmaps.get(&bitmap.handle))
{
let repeat = if !*is_repeating {
// NOTE: The WebGL backend does clamping in this case, just like
// Flash Player, but CanvasPattern has no such option...
"no-repeat"
let bitmap = if let Ok(bitmap) = create_bitmap_pattern(
*id,
*matrix,
*is_smoothed,
*is_repeating,
bitmap_source,
bitmaps,
context,
) {
bitmap
} else {
"repeat"
};
let bitmap_pattern = if let Ok(Some(bitmap_pattern)) =
context.create_pattern_with_html_canvas_element(&bitmap.canvas, repeat)
{
bitmap_pattern
} else {
log::warn!("Unable to create bitmap pattern for bitmap ID {}", id);
continue;
};
bitmap_pattern.set_transform(matrix.to_dom_matrix().unchecked_ref());
CanvasFillStyle::Pattern(bitmap_pattern, *is_smoothed)
} else {
log::error!("Couldn't fill shape with unknown bitmap {}", id);
CanvasFillStyle::Color(CanvasColor("rgba(0,0,0,0)".to_string(), 0, 0, 0, 0))
}
CanvasFillStyle::Bitmap(bitmap)
}
};
let canvas_path = Path2d::new().unwrap();
canvas_path.add_path_with_transformation(
&draw_commands_to_path2d(commands, is_closed),
bounds_viewbox_matrix.unchecked_ref(),
);
match path {
DrawPath::Fill { .. } => {
canvas_data.0.push(CanvasDrawCommand::Fill {
path: canvas_path,
fill_style,
});
}
DrawPath::Stroke { style, .. } => {
DrawPath::Stroke {
commands,
style,
is_closed,
} => {
let canvas_path = Path2d::new().unwrap();
canvas_path.add_path_with_transformation(
&draw_commands_to_path2d(commands, *is_closed),
bounds_viewbox_matrix.unchecked_ref(),
);
let stroke_style = match style.fill_style() {
FillStyle::Color(color) => CanvasStrokeStyle::Color(color.into()),
FillStyle::LinearGradient(gradient) => {
CanvasStrokeStyle::Gradient(gradient.clone(), None)
}
FillStyle::RadialGradient(gradient) => {
CanvasStrokeStyle::Gradient(gradient.clone(), Some(0.0))
}
FillStyle::FocalGradient {
gradient,
focal_point,
} => CanvasStrokeStyle::Gradient(gradient.clone(), Some(focal_point.to_f64())),
FillStyle::Bitmap {
id,
matrix,
is_smoothed,
is_repeating,
} => {
let bitmap = if let Ok(bitmap) = create_bitmap_pattern(
*id,
*matrix,
*is_smoothed,
*is_repeating,
bitmap_source,
bitmaps,
context,
) {
bitmap
} else {
continue;
};
CanvasStrokeStyle::Bitmap(bitmap)
}
};
let line_cap = match style.start_cap() {
LineCapStyle::Round => "round",
LineCapStyle::Square => "square",
@ -792,7 +877,7 @@ fn swf_shape_to_canvas_commands(
canvas_data.0.push(CanvasDrawCommand::Stroke {
path: canvas_path,
line_width: style.width().to_pixels(),
stroke_style: fill_style,
stroke_style,
line_cap: line_cap.to_string(),
line_join: line_join.to_string(),
miter_limit: miter_limit as f64 / 20.0,
@ -813,14 +898,14 @@ fn create_linear_gradient(
context: &CanvasRenderingContext2d,
gradient: &swf::Gradient,
is_fill: bool,
) -> Result<CanvasFillStyle, JsError> {
) -> Result<Gradient, JsError> {
// Canvas linear gradients are configured via the line endpoints, so we only need
// to transform it if the basis is not orthogonal (skew in the transform).
let transformed = if is_fill {
let dot = gradient.matrix.a * gradient.matrix.c + gradient.matrix.b * gradient.matrix.d;
dot.to_f32().abs() > GRADIENT_TRANSFORM_THRESHOLD
} else {
// TODO: Gradient transforms don't work correctly with strokes.
// Complex gradient transforms can't apply to strokes; fall back to simple transforms.
false
};
let create_fn = |matrix: swf::Matrix, gradient_scale: f64| {
@ -844,7 +929,7 @@ fn create_radial_gradient(
gradient: &swf::Gradient,
focal_point: f64,
is_fill: bool,
) -> Result<CanvasFillStyle, JsError> {
) -> Result<Gradient, JsError> {
// Canvas radial gradients can not be elliptical or skewed, so transform if there
// is a non-uniform scale or skew.
// A scale rotation matrix is always of the form:
@ -854,7 +939,7 @@ fn create_radial_gradient(
(gradient.matrix.a - gradient.matrix.d).to_f32().abs() > GRADIENT_TRANSFORM_THRESHOLD
|| (gradient.matrix.b + gradient.matrix.c).to_f32().abs() > GRADIENT_TRANSFORM_THRESHOLD
} else {
// TODO: Gradient transforms don't work correctly with strokes.
// Complex gradient transforms can't apply to strokes; fall back to simple transforms.
false
};
let create_fn = |matrix: swf::Matrix, gradient_scale: f64| {
@ -893,7 +978,7 @@ fn swf_to_canvas_gradient(
swf_gradient: &swf::Gradient,
transformed: bool,
mut create_gradient_fn: impl FnMut(swf::Matrix, f64) -> Result<CanvasGradient, JsError>,
) -> Result<CanvasFillStyle, JsError> {
) -> Result<Gradient, JsError> {
let matrix = if transformed {
// When we are rendering a complex gradient, the gradient transform is handled later by
// transforming the path before rendering; so use the indentity matrix here.
@ -983,14 +1068,15 @@ fn swf_to_canvas_gradient(
}
}
if transformed {
Ok(Gradient {
gradient: canvas_gradient,
transform: transformed.then(|| {
// When we render this gradient, we will push the gradient's transform to the canvas,
// and then transform the path itself by the inverse.
let matrix = swf_gradient.matrix.to_dom_matrix();
let inverse_gradient_matrix = matrix.inverse();
Ok(CanvasFillStyle::TransformedGradient(TransformedGradient {
gradient: canvas_gradient,
gradient_matrix: [
let inverse_matrix = matrix.inverse();
GradientTransform {
matrix: [
matrix.a(),
matrix.b(),
matrix.c(),
@ -998,10 +1084,51 @@ fn swf_to_canvas_gradient(
matrix.e(),
matrix.f(),
],
inverse_gradient_matrix,
}))
inverse_matrix,
}
}),
})
}
/// Converts an SWF bitmap fill to a canvas pattern.
fn create_bitmap_pattern(
id: swf::CharacterId,
matrix: swf::Matrix,
is_smoothed: bool,
is_repeating: bool,
bitmap_source: &dyn BitmapSource,
bitmaps: &FnvHashMap<BitmapHandle, BitmapData>,
context: &CanvasRenderingContext2d,
) -> Result<CanvasBitmap, Error> {
if let Some(bitmap) = bitmap_source
.bitmap(id)
.and_then(|bitmap| bitmaps.get(&bitmap.handle))
{
let repeat = if !is_repeating {
// NOTE: The WebGL backend does clamping in this case, just like
// Flash Player, but CanvasPattern has no such option...
"no-repeat"
} else {
Ok(CanvasFillStyle::Gradient(canvas_gradient))
"repeat"
};
let pattern = match context.create_pattern_with_html_canvas_element(&bitmap.canvas, repeat)
{
Ok(Some(pattern)) => pattern,
_ => {
log::warn!("Unable to create bitmap pattern for bitmap ID {}", id);
return Err("Unable to create bitmap pattern".into());
}
};
pattern.set_transform(matrix.to_dom_matrix().unchecked_ref());
Ok(CanvasBitmap {
pattern,
matrix: matrix.into(),
smoothed: is_smoothed,
})
} else {
log::warn!("Couldn't fill shape with unknown bitmap {}", id);
Err("Unable to create bitmap pattern".into())
}
}