Skip to content

Commit

Permalink
Improve drop shadow effect accuracy (#2523)
Browse files Browse the repository at this point in the history
These changes make several improvements to how drop shadow effects are displayed:
- Drop shadows now take parent alpha into account. This means that if a layer or fill has an animated opacity, the drop shadow will multiply that opacity with its own.
- Adds drop shadow support to Image layers. There are some visual bugs with how the shadows are rendered, however.
- Applies a constant scale factor to the distance and softness values from the Lottie file before passing them to `Paint.setShadowLayer()` to more closely match how they are displayed in After Effects. See airbnb/lottie-ios#2175 for similar changes on iOS.
- Adds three snapshot test files for distance, softness, and alpha validation.

Distance test file:
<img src="https://github.com/user-attachments/assets/aa559e60-0d8f-403b-acd0-fa571d6dcff5" width=400>

Softness test file:
<img src="https://github.com/user-attachments/assets/faa9819f-6584-49f5-91b7-7462213044f5" width=400>

Example of Image layer shadow bug, right side is how it should look (capture from After Effects):
<img width="1824" alt="image" src="https://github.com/user-attachments/assets/a3fb85c3-a5d1-4a6b-bbda-ff4e5ebd2fb5">

Co-authored-by: Gabriel Peal <gabriel@gpeal.com>
  • Loading branch information
allenchen1154 and gpeal authored Aug 4, 2024
1 parent 3f39884 commit 33d7e1c
Show file tree
Hide file tree
Showing 13 changed files with 3,626 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ public abstract class BaseStrokeContent
blurMaskFilterRadius = blurRadius;
}
if (dropShadowAnimation != null) {
dropShadowAnimation.applyTo(paint);
dropShadowAnimation.applyTo(paint, parentMatrix, Utils.mixOpacities(parentAlpha, alpha));
}

for (int i = 0; i < pathGroups.size(); i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.airbnb.lottie.model.content.ShapeFill;
import com.airbnb.lottie.model.layer.BaseLayer;
import com.airbnb.lottie.utils.MiscUtils;
import com.airbnb.lottie.utils.Utils;
import com.airbnb.lottie.value.LottieValueCallback;

import java.util.ArrayList;
Expand Down Expand Up @@ -120,7 +121,7 @@ public FillContent(final LottieDrawable lottieDrawable, BaseLayer layer, ShapeFi
blurMaskFilterRadius = blurRadius;
}
if (dropShadowAnimation != null) {
dropShadowAnimation.applyTo(paint);
dropShadowAnimation.applyTo(paint, parentMatrix, Utils.mixOpacities(parentAlpha, alpha));
}

path.reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.airbnb.lottie.model.content.GradientType;
import com.airbnb.lottie.model.layer.BaseLayer;
import com.airbnb.lottie.utils.MiscUtils;
import com.airbnb.lottie.utils.Utils;
import com.airbnb.lottie.value.LottieValueCallback;

import java.util.ArrayList;
Expand Down Expand Up @@ -150,13 +151,14 @@ public GradientFillContent(final LottieDrawable lottieDrawable, LottieCompositio
}
blurMaskFilterRadius = blurRadius;
}
if (dropShadowAnimation != null) {
dropShadowAnimation.applyTo(paint);
}

int alpha = (int) ((parentAlpha / 255f * opacityAnimation.getValue() / 100f) * 255);
paint.setAlpha(clamp(alpha, 0, 255));

if (dropShadowAnimation != null) {
dropShadowAnimation.applyTo(paint, parentMatrix, Utils.mixOpacities(parentAlpha, alpha));
}

canvas.drawPath(path, paint);
if (L.isTraceEnabled()) {
L.endSection("GradientFillContent#draw");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
package com.airbnb.lottie.animation.keyframe;

import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;

import androidx.annotation.Nullable;

import com.airbnb.lottie.model.layer.BaseLayer;
import com.airbnb.lottie.parser.DropShadowEffect;
import com.airbnb.lottie.value.LottieFrameInfo;
import com.airbnb.lottie.value.LottieValueCallback;


public class DropShadowKeyframeAnimation implements BaseKeyframeAnimation.AnimationListener {
private static final double DEG_TO_RAD = Math.PI / 180.0;
private static final float DEG_TO_RAD = (float) (Math.PI / 180.0);

private final BaseLayer layer;
private final BaseKeyframeAnimation.AnimationListener listener;
private final BaseKeyframeAnimation<Integer, Integer> color;
private final BaseKeyframeAnimation<Float, Float> opacity;
private final BaseKeyframeAnimation<Float, Float> direction;
private final BaseKeyframeAnimation<Float, Float> distance;
private final BaseKeyframeAnimation<Float, Float> radius;
private final FloatKeyframeAnimation opacity;
private final FloatKeyframeAnimation direction;
private final FloatKeyframeAnimation distance;
private final FloatKeyframeAnimation radius;

// Cached paint values.
private float paintRadius = Float.NaN;
private float paintX = Float.NaN;
private float paintY = Float.NaN;
// 0 is a valid color but it is transparent so it will not draw anything anyway.
private int paintColor = 0;

private boolean isDirty = true;
private final float[] matrixValues = new float[9];

public DropShadowKeyframeAnimation(BaseKeyframeAnimation.AnimationListener listener, BaseLayer layer, DropShadowEffect dropShadowEffect) {
this.listener = listener;
this.layer = layer;
color = dropShadowEffect.getColor().createAnimation();
color.addUpdateListener(this);
layer.addAnimation(color);
Expand All @@ -42,24 +51,49 @@ public DropShadowKeyframeAnimation(BaseKeyframeAnimation.AnimationListener liste
}

@Override public void onValueChanged() {
isDirty = true;
listener.onValueChanged();
}

public void applyTo(Paint paint) {
if (!isDirty) {
return;
}
isDirty = false;

double directionRad = ((double) direction.getValue()) * DEG_TO_RAD;
/**
* Applies a shadow to the provided Paint object, which will be applied to the Canvas behind whatever is drawn
* (a shape, bitmap, path, etc.)
* @param parentAlpha A value between 0 and 255 representing the combined alpha of all parents of this drop shadow effect.
* E.g. The layer via transform, the fill/stroke via its opacity, etc.
*/
public void applyTo(Paint paint, Matrix parentMatrix, int parentAlpha) {
float directionRad = this.direction.getFloatValue() * DEG_TO_RAD;
float distance = this.distance.getValue();
float x = ((float) Math.sin(directionRad)) * distance;
float y = ((float) Math.cos(directionRad + Math.PI)) * distance;
float rawX = ((float) Math.sin(directionRad)) * distance;
float rawY = ((float) Math.cos(directionRad + Math.PI)) * distance;

// The x and y coordinates are relative to the shape that is being drawn.
// The distance in the animation is relative to the original size of the shape.
// If the shape will be drawn scaled, we need to scale the distance we draw the shadow.
layer.transform.getMatrix().getValues(matrixValues);
float layerScaleX = matrixValues[Matrix.MSCALE_X];
float layerScaleY = matrixValues[Matrix.MSCALE_Y];
parentMatrix.getValues(matrixValues);
float parentScaleX = matrixValues[Matrix.MSCALE_X];
float parentScaleY = matrixValues[Matrix.MSCALE_Y];
float scaleX = parentScaleX / layerScaleX;
float scaleY = parentScaleY / layerScaleY;
float x = rawX * scaleX;
float y = rawY * scaleY;

int baseColor = color.getValue();
int opacity = Math.round(this.opacity.getValue());
int opacity = Math.round(this.opacity.getValue() * parentAlpha / 255f);
int color = Color.argb(opacity, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor));
float radius = this.radius.getValue();

// Paint.setShadowLayer() removes the shadow if radius is 0, so we use a small nonzero value in that case
float radius = Math.max(this.radius.getValue() * scaleX, Float.MIN_VALUE);

if (paintRadius == radius && paintX == x && paintY == y && paintColor == color) {
return;
}
paintRadius = radius;
paintX = x;
paintY = y;
paintColor = color;
paint.setShadowLayer(radius, x, y, color);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.airbnb.lottie.model.animatable;

import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation;
import com.airbnb.lottie.value.Keyframe;

Expand All @@ -12,7 +11,7 @@ public AnimatableFloatValue(List<Keyframe<Float>> keyframes) {
super(keyframes);
}

@Override public BaseKeyframeAnimation<Float, Float> createAnimation() {
@Override public FloatKeyframeAnimation createAnimation() {
return new FloatKeyframeAnimation(keyframes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ static BaseLayer forModel(
private List<BaseLayer> parentLayers;

private final List<BaseKeyframeAnimation<?, ?>> animations = new ArrayList<>();
final TransformKeyframeAnimation transform;
public final TransformKeyframeAnimation transform;
private boolean visible = true;

private boolean outlineMasksAndMattes;
Expand Down
10 changes: 10 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.animation.LPaint;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.DropShadowKeyframeAnimation;
import com.airbnb.lottie.animation.keyframe.ValueCallbackKeyframeAnimation;
import com.airbnb.lottie.utils.Utils;
import com.airbnb.lottie.value.LottieValueCallback;
Expand All @@ -28,10 +29,15 @@ public class ImageLayer extends BaseLayer {
@Nullable private final LottieImageAsset lottieImageAsset;
@Nullable private BaseKeyframeAnimation<ColorFilter, ColorFilter> colorFilterAnimation;
@Nullable private BaseKeyframeAnimation<Bitmap, Bitmap> imageAnimation;
@Nullable private DropShadowKeyframeAnimation dropShadowAnimation;

ImageLayer(LottieDrawable lottieDrawable, Layer layerModel) {
super(lottieDrawable, layerModel);
lottieImageAsset = lottieDrawable.getLottieImageAssetForId(layerModel.getRefId());

if (getDropShadowEffect() != null) {
dropShadowAnimation = new DropShadowKeyframeAnimation(this, this, getDropShadowEffect());
}
}

@Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
Expand All @@ -54,6 +60,10 @@ public class ImageLayer extends BaseLayer {
dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density));
}

if (dropShadowAnimation != null) {
dropShadowAnimation.applyTo(paint, parentMatrix, parentAlpha);
}

canvas.drawBitmap(bitmap, src, dst, paint);
canvas.restore();
}
Expand Down
7 changes: 7 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/utils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,13 @@ public static void saveLayerCompat(Canvas canvas, RectF rect, Paint paint, int f
}
}

/**
* Multiplies 2 opacities that are 0-255.
*/
public static int mixOpacities(int opacity1, int opacity2) {
return (int) ((opacity1 / 255f * opacity2 / 255f) * 255f);
}

/**
* For testing purposes only. DO NOT USE IN PRODUCTION.
*/
Expand Down
Loading

0 comments on commit 33d7e1c

Please sign in to comment.