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