canvas: Scale stroke width based on shape transform
This commit is contained in:
parent
56adcc5665
commit
1980d6f420
|
@ -1321,3 +1321,48 @@ pub fn swf_glyph_to_shape(glyph: &swf::Glyph) -> swf::Shape {
|
|||
shape: glyph.shape_records.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale mode used by strokes in a shape.
|
||||
///
|
||||
/// Determines how the line thickness is affected by the shape's transform.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum LineScaleMode {
|
||||
None = 0,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Both,
|
||||
}
|
||||
|
||||
/// Helper type for calculating line widths for a transformed shape.
|
||||
pub struct LineScales<'a> {
|
||||
matrix: &'a Matrix,
|
||||
scales: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl<'a> LineScales<'a> {
|
||||
/// Create a new line scaler for the given matrix.
|
||||
#[inline]
|
||||
pub fn new(matrix: &'a Matrix) -> Self {
|
||||
Self {
|
||||
matrix,
|
||||
scales: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the final width of a line after transformation.
|
||||
#[inline]
|
||||
pub fn transform_width(&mut self, width: f32, scale_mode: LineScaleMode) -> f32 {
|
||||
// Lazily calculate the scale to avoid doing so for shapes that have no strokes.
|
||||
let scales = self.scales.get_or_insert_with(|| {
|
||||
let line_scale_x = f32::abs(self.matrix.a + self.matrix.c);
|
||||
let line_scale_y = f32::abs(self.matrix.b + self.matrix.d);
|
||||
let line_scale =
|
||||
((line_scale_x * line_scale_x + line_scale_y * line_scale_y) / 2.0).sqrt();
|
||||
[1.0, line_scale_x, line_scale_y, line_scale]
|
||||
});
|
||||
let scaled_width = width * scales[scale_mode as usize];
|
||||
// Flash draws all strokes with a minimum width of 1 pixel.
|
||||
// This usually occurs in "hairline" strokes (exported with width of 1 twip).
|
||||
scaled_width.max(1.0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use ruffle_core::backend::render::{
|
|||
};
|
||||
use ruffle_core::color_transform::ColorTransform;
|
||||
use ruffle_core::matrix::Matrix;
|
||||
use ruffle_core::shape_utils::{DistilledShape, DrawCommand};
|
||||
use ruffle_core::shape_utils::{DistilledShape, DrawCommand, LineScaleMode, LineScales};
|
||||
use ruffle_web_common::{JsError, JsResult};
|
||||
use wasm_bindgen::{Clamped, JsCast};
|
||||
use web_sys::{
|
||||
|
@ -58,6 +58,7 @@ enum CanvasDrawCommand {
|
|||
line_cap: String,
|
||||
line_join: String,
|
||||
miter_limit: f64,
|
||||
scale_mode: LineScaleMode,
|
||||
},
|
||||
|
||||
/// A command to fill a path with a given style.
|
||||
|
@ -383,6 +384,8 @@ impl RenderBackend for WebCanvasRenderBackend {
|
|||
fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform) {
|
||||
match &self.mask_state {
|
||||
MaskState::DrawContent => {
|
||||
let mut line_scale = LineScales::new(&transform.matrix);
|
||||
let dom_matrix = transform.matrix.to_dom_matrix();
|
||||
self.set_transform(&transform.matrix);
|
||||
if let Some(shape) = self.shapes.get(shape.0) {
|
||||
for command in shape.0.iter() {
|
||||
|
@ -457,47 +460,59 @@ impl RenderBackend for WebCanvasRenderBackend {
|
|||
line_cap,
|
||||
line_join,
|
||||
miter_limit,
|
||||
scale_mode,
|
||||
} => {
|
||||
// Canvas.setTransform ends up transforming the stroke geometry itself (including joins/endcaps).
|
||||
// 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();
|
||||
let transformed_path = Path2d::new().unwrap();
|
||||
transformed_path
|
||||
.add_path_with_transformation(path, dom_matrix.unchecked_ref());
|
||||
|
||||
// Set stroke parameters.
|
||||
self.context.set_line_cap(line_cap);
|
||||
self.context.set_line_join(line_join);
|
||||
self.context.set_miter_limit(*miter_limit);
|
||||
self.context.set_line_width(*line_width);
|
||||
let line_width =
|
||||
line_scale.transform_width(*line_width as f32, *scale_mode);
|
||||
self.context.set_line_width(line_width.into());
|
||||
match stroke_style {
|
||||
CanvasFillStyle::Color(color) => {
|
||||
let color =
|
||||
color.color_transform(&transform.color_transform);
|
||||
self.context.set_stroke_style(&color.0.into());
|
||||
self.context.stroke_with_path(path);
|
||||
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(path);
|
||||
self.context.stroke_with_path(&transformed_path);
|
||||
self.clear_color_filter();
|
||||
}
|
||||
CanvasFillStyle::TransformedGradient(gradient) => {
|
||||
self.set_color_filter(transform);
|
||||
self.context.set_stroke_style(&gradient.gradient);
|
||||
self.context.stroke_with_path(path);
|
||||
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();
|
||||
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);
|
||||
self.context.stroke_with_path(path);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -764,15 +779,6 @@ fn swf_shape_to_canvas_commands(
|
|||
});
|
||||
}
|
||||
DrawPath::Stroke { style, .. } => {
|
||||
// Flash always renders strokes with a minimum width of 1 pixel (20 twips).
|
||||
// Additionally, many SWFs use the "hairline" stroke setting, which sets the stroke's width
|
||||
// to 1 twip. Because of the minimum, this will effectively make the stroke nearly-always render
|
||||
// as 1 pixel wide.
|
||||
// SVG doesn't have a minimum and can render strokes at fractional widths, so these hairline
|
||||
// strokes end up rendering very faintly if we use the actual width of 1 twip.
|
||||
// Therefore, we clamp the stroke width to 1 pixel (20 twips). This won't be 100% accurate
|
||||
// if the shape is scaled, but it looks much closer to the Flash Player.
|
||||
let line_width = std::cmp::max(style.width().get(), 20);
|
||||
let line_cap = match style.start_cap() {
|
||||
LineCapStyle::Round => "round",
|
||||
LineCapStyle::Square => "square",
|
||||
|
@ -785,11 +791,17 @@ fn swf_shape_to_canvas_commands(
|
|||
};
|
||||
canvas_data.0.push(CanvasDrawCommand::Stroke {
|
||||
path: canvas_path,
|
||||
line_width: line_width as f64 / 20.0,
|
||||
line_width: style.width().to_pixels(),
|
||||
stroke_style: fill_style,
|
||||
line_cap: line_cap.to_string(),
|
||||
line_join: line_join.to_string(),
|
||||
miter_limit: miter_limit as f64 / 20.0,
|
||||
scale_mode: match (style.allow_scale_x(), style.allow_scale_y()) {
|
||||
(false, false) => LineScaleMode::None,
|
||||
(true, false) => LineScaleMode::Horizontal,
|
||||
(false, true) => LineScaleMode::Vertical,
|
||||
(true, true) => LineScaleMode::Both,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue