Skip to content

Commit

Permalink
Fixed scaling issues with shadow blur when applied to a canvas transf…
Browse files Browse the repository at this point in the history
…ormed with rotation and/or skew matrices. (#174)

* Fixed scaling issues with shadow blur when applied to a canvas transformed with rotation and/or skew matrices. Add visual tests.

* move `render_shadow` into `render_to_canvas` as closure

* add unit test for the shadowBlur + transforms fix

---------

Co-authored-by: Christian Swinehart <drafting@samizdat.co>
  • Loading branch information
mpaperno and samizdatco authored Oct 28, 2024
1 parent 891d140 commit b0b5c6f
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 259 deletions.
61 changes: 38 additions & 23 deletions src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ use std::cell::RefCell;
use std::sync::{Arc, Mutex, MutexGuard};
use neon::prelude::*;
use skia_safe::{Canvas as SkCanvas, Surface, Paint, Path, PathOp, Image, ImageInfo, Contains,
Matrix, Rect, Point, IPoint, Size, ISize, Color, Color4f, ColorType, Data,
Rect, Point, IPoint, Size, ISize, Color, Color4f, ColorType, ColorSpace, Data,
PaintStyle, BlendMode, AlphaType, ClipOp, PictureRecorder, Picture, Drawable,
image::CachingHint, images, image_filters, dash_path_effect, path_1d_path_effect};
use skia_safe::matrix::{ Matrix, TypeMask };
use skia_safe::textlayout::{ParagraphStyle, TextStyle};
use skia_safe::canvas::SrcRectConstraint::Strict;
use skia_safe::path::FillType;
Expand Down Expand Up @@ -204,6 +205,16 @@ impl Context2D{
pub fn render_to_canvas<F>(&self, paint:&Paint, f:F)
where F:Fn(&SkCanvas, &Paint)
{
let render_shadow = |canvas:&SkCanvas, paint:&Paint|{
if let Some(shadow_paint) = self.paint_for_shadow(paint){
canvas.save();
canvas.set_matrix(&Matrix::translate(self.state.shadow_offset).into());
canvas.concat(&self.state.matrix);
f(canvas, &shadow_paint);
canvas.restore();
}
};

match self.state.global_composite_operation{
BlendMode::SrcIn | BlendMode::SrcOut |
BlendMode::DstIn | BlendMode::DstOut |
Expand All @@ -216,20 +227,12 @@ impl Context2D{
layer_recorder.begin_recording(self.bounds, None);
if let Some(layer) = layer_recorder.recording_canvas() {
// draw the dropshadow (if applicable)
if let Some(shadow_paint) = self.paint_for_shadow(&layer_paint){
layer.save();
layer.set_matrix(&Matrix::translate(self.state.shadow_offset).into());
layer.concat(&self.state.matrix);
f(layer, &shadow_paint);
layer.restore();
}

render_shadow(layer, &layer_paint);
// draw normally
layer.set_matrix(&self.state.matrix.into());
f(layer, &layer_paint);
}


// transfer the picture contents to the canvas in a single operation, applying the blend
// mode to the whole canvas (regardless of the bounds of the text/path being drawn)
if let Some(pict) = layer_recorder.finish_recording_as_picture(Some(&self.bounds)){
Expand All @@ -247,14 +250,8 @@ impl Context2D{
},
_ => {
self.with_canvas(|canvas| {
if let Some(shadow_paint) = self.paint_for_shadow(paint){
canvas.save();
canvas.set_matrix(&Matrix::translate(self.state.shadow_offset).into());
canvas.concat(&self.state.matrix);
f(canvas, &shadow_paint);
canvas.restore();
}

// draw the dropshadow (if applicable)
render_shadow(canvas, paint);
// draw with the normal paint
f(canvas, paint);
});
Expand Down Expand Up @@ -536,7 +533,7 @@ impl Context2D{
false => {
let mut traced_path = Path::default();
fill_path_with_paint(path, &paint, &mut traced_path, None, None);
traced_path
traced_path
}
};
path_1d_path_effect::new(
Expand All @@ -563,15 +560,33 @@ impl Context2D{
}

pub fn paint_for_shadow(&self, base_paint:&Paint) -> Option<Paint> {
let State {shadow_color, shadow_blur, shadow_offset, ..} = self.state;
let State {shadow_color, mut shadow_blur, shadow_offset, ..} = self.state;
if shadow_color.a() == 0 || (shadow_blur == 0.0 && shadow_offset.is_zero()){
return None
}

let sigma_x = shadow_blur / (2.0 * self.state.matrix.scale_x());
let sigma_y = shadow_blur / (2.0 * self.state.matrix.scale_y());
// Per spec, sigma is exactly half the blur radius:
// https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
shadow_blur *= 0.5;
let mut sigma = Point::new(shadow_blur, shadow_blur);
// Apply scaling from the current transform matrix to blur radius, if there is any of either.
if self.state.matrix.get_type().contains(TypeMask::SCALE) && !almost_zero(shadow_blur) {
// Decompose the matrix to just the scaling factors (matrix.scale_x/y() methods just return M11/M22 values)
if let Some(scale) = self.state.matrix.decompose_scale(None) {
if almost_zero(scale.width) {
sigma.x = 0.0;
} else {
sigma.x /= scale.width as f32;
}
if almost_zero(scale.height) {
sigma.y = 0.0;
} else {
sigma.y /= scale.height as f32;
}
}
}
let mut paint = base_paint.clone();
paint.set_image_filter(image_filters::drop_shadow_only((0.0, 0.0), (sigma_x, sigma_y), shadow_color, None, None, None));
paint.set_image_filter(image_filters::drop_shadow_only((0.0, 0.0), (sigma.x, sigma.y), shadow_color, ColorSpace::new_srgb(), None, None));
Some(paint)
}

Expand Down
4 changes: 4 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub fn almost_equal(a: f32, b: f32) -> bool{
(a-b).abs() < 0.00001
}

pub fn almost_zero(a: f32) -> bool{
a.abs() < 0.00001
}

pub fn to_degrees(radians: f32) -> f32{
radians / PI * 180.0
}
Expand Down
17 changes: 17 additions & 0 deletions test/context2d.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,23 @@ describe("Context2D", ()=>{
expect(pixel(10, 10)).toEqual([0, 162, 213, 245])
})

test('shadow', async() => {
const sin = Math.sin(1.15*Math.PI)
const cos = Math.cos(1.15*Math.PI)
ctx.translate(150, 150)
ctx.transform(cos, sin, -sin, cos, 0, 0)

ctx.shadowColor = '#000'
ctx.shadowBlur = 5
ctx.shadowOffsetX = 10
ctx.shadowOffsetY = 10
ctx.fillStyle = '#eee'
ctx.fillRect(25, 25, 65, 10)

// ensure that the shadow is actually fuzzy despite the transforms
expect(pixel(143, 117)).not.toEqual(BLACK)
})

test("clip()", () => {
ctx.fillStyle = 'white'
ctx.fillRect(0, 0, 2, 2)
Expand Down
Loading

0 comments on commit b0b5c6f

Please sign in to comment.