From 2bb80251ae9fbc113f89abd1ed5c1a5c807c57c5 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Wed, 15 Nov 2023 01:49:12 -0500 Subject: [PATCH] Implement auto-orient (#2416) Fixes #1401 --- .../keyframe/TransformKeyframeAnimation.java | 42 +++++++++++++++---- .../model/animatable/AnimatableTransform.java | 13 ++++++ .../com/airbnb/lottie/parser/LayerParser.java | 37 ++++++++++------ .../src/main/assets/Tests/AutoOrient.json | 1 + 4 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 snapshot-tests/src/main/assets/Tests/AutoOrient.json diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/TransformKeyframeAnimation.java b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/TransformKeyframeAnimation.java index 590113562c..13a63ee60d 100644 --- a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/TransformKeyframeAnimation.java +++ b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/TransformKeyframeAnimation.java @@ -44,12 +44,16 @@ public class TransformKeyframeAnimation { @Nullable private BaseKeyframeAnimation startOpacity; @Nullable private BaseKeyframeAnimation endOpacity; + private final boolean autoOrient; + + public TransformKeyframeAnimation(AnimatableTransform animatableTransform) { anchorPoint = animatableTransform.getAnchorPoint() == null ? null : animatableTransform.getAnchorPoint().createAnimation(); position = animatableTransform.getPosition() == null ? null : animatableTransform.getPosition().createAnimation(); scale = animatableTransform.getScale() == null ? null : animatableTransform.getScale().createAnimation(); rotation = animatableTransform.getRotation() == null ? null : animatableTransform.getRotation().createAnimation(); skew = animatableTransform.getSkew() == null ? null : (FloatKeyframeAnimation) animatableTransform.getSkew().createAnimation(); + autoOrient = animatableTransform.isAutoOrient(); if (skew != null) { skewMatrix1 = new Matrix(); skewMatrix2 = new Matrix(); @@ -174,16 +178,36 @@ public Matrix getMatrix() { } } - BaseKeyframeAnimation rotation = this.rotation; - if (rotation != null) { - float rotationValue; - if (rotation instanceof ValueCallbackKeyframeAnimation) { - rotationValue = rotation.getValue(); - } else { - rotationValue = ((FloatKeyframeAnimation) rotation).getFloatValue(); + // If autoOrient is true, the rotation should follow the derivative of the position rather + // than the rotation property. + if (autoOrient) { + if (position != null) { + float currentProgress = position.getProgress(); + PointF startPosition = position.getValue(); + // Store the start X and Y values because the pointF will be overwritten by the next getValue call. + float startX = startPosition.x; + float startY = startPosition.y; + // 1) Find the next position value. + // 2) Create a vector from the current position to the next position. + // 3) Find the angle of that vector to the X axis (0 degrees). + position.setProgress(currentProgress + 0.0001f); + PointF nextPosition = position.getValue(); + position.setProgress(currentProgress); + double rotationValue = Math.toDegrees(Math.atan2(nextPosition.y - startY, nextPosition.x - startX)); + matrix.preRotate((float) rotationValue); } - if (rotationValue != 0f) { - matrix.preRotate(rotationValue); + } else { + BaseKeyframeAnimation rotation = this.rotation; + if (rotation != null) { + float rotationValue; + if (rotation instanceof ValueCallbackKeyframeAnimation) { + rotationValue = rotation.getValue(); + } else { + rotationValue = ((FloatKeyframeAnimation) rotation).getFloatValue(); + } + if (rotationValue != 0f) { + matrix.preRotate(rotationValue); + } } } diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java index 14bd173eef..d88e11f4c9 100644 --- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java +++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java @@ -34,6 +34,8 @@ public class AnimatableTransform implements ModifierContent, ContentModel { @Nullable private final AnimatableFloatValue endOpacity; + private boolean autoOrient = false; + public AnimatableTransform() { this(null, null, null, null, null, null, null, null, null); } @@ -54,6 +56,13 @@ public AnimatableTransform(@Nullable AnimatablePathValue anchorPoint, this.skewAngle = skewAngle; } + /** + * This is set as a property of the layer so it is parsed and set separately. + */ + public void setAutoOrient(boolean autoOrient) { + this.autoOrient = autoOrient; + } + @Nullable public AnimatablePathValue getAnchorPoint() { return anchorPoint; @@ -99,6 +108,10 @@ public AnimatableFloatValue getSkewAngle() { return skewAngle; } + public boolean isAutoOrient() { + return autoOrient; + } + public TransformKeyframeAnimation createAnimation() { return new TransformKeyframeAnimation(this); } diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java index e065cf0f10..906e695cf5 100644 --- a/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java +++ b/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java @@ -27,29 +27,30 @@ private LayerParser() { } private static final JsonReader.Options NAMES = JsonReader.Options.of( - "nm", // 0 - "ind", // 1 - "refId", // 2 - "ty", // 3 + "nm", // 0 + "ind", // 1 + "refId", // 2 + "ty", // 3 "parent", // 4 - "sw", // 5 - "sh", // 6 - "sc", // 7 - "ks", // 8 - "tt", // 9 + "sw", // 5 + "sh", // 6 + "sc", // 7 + "ks", // 8 + "tt", // 9 "masksProperties", // 10 "shapes", // 11 - "t", // 12 + "t", // 12 "ef", // 13 "sr", // 14 "st", // 15 - "w", // 16 - "h", // 17 + "w", // 16 + "h", // 17 "ip", // 18 "op", // 19 "tm", // 20 "cl", // 21 - "hd" // 22 + "hd", // 22 + "ao" // 23 ); public static Layer parse(LottieComposition composition) { @@ -93,6 +94,7 @@ public static Layer parse(JsonReader reader, LottieComposition composition) thro boolean hidden = false; BlurEffect blurEffect = null; DropShadowEffect dropShadowEffect = null; + boolean autoOrient = false; Layer.MatteType matteType = Layer.MatteType.NONE; AnimatableTransform transform = null; @@ -256,6 +258,9 @@ public static Layer parse(JsonReader reader, LottieComposition composition) thro case 22: hidden = reader.nextBoolean(); break; + case 23: + autoOrient = reader.nextInt() == 1; + break; default: reader.skipName(); reader.skipValue(); @@ -284,6 +289,12 @@ public static Layer parse(JsonReader reader, LottieComposition composition) thro composition.addWarning("Convert your Illustrator layers to shape layers."); } + if (autoOrient) { + if (transform == null) { + transform = new AnimatableTransform(); + } + transform.setAutoOrient(autoOrient); + } return new Layer(shapes, composition, layerName, layerId, layerType, parentId, refId, masks, transform, solidWidth, solidHeight, solidColor, timeStretch, startFrame, preCompWidth, preCompHeight, text, textProperties, inOutKeyframes, matteType, diff --git a/snapshot-tests/src/main/assets/Tests/AutoOrient.json b/snapshot-tests/src/main/assets/Tests/AutoOrient.json new file mode 100644 index 0000000000..14ced3e77b --- /dev/null +++ b/snapshot-tests/src/main/assets/Tests/AutoOrient.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":29.9700012207031,"ip":0,"op":284.000011567557,"w":400,"h":400,"nm":"ao","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"shape","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.641,"y":0.641},"o":{"x":0.167,"y":0.167},"t":0,"s":[68.5,44.5,0],"to":[2.395,1.173,0],"ti":[-52,-114,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.438,"y":0.438},"t":83.533,"s":[304.5,148.5,0],"to":[-8,108,0],"ti":[-2.164,-1.328,0]},{"i":{"x":0.575,"y":0.575},"o":{"x":0.167,"y":0.167},"t":114,"s":[340.035,204.247,0],"to":[60.399,77.002,0],"ti":[118,-2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.407,"y":0.407},"t":189.251,"s":[240.5,270.5,0],"to":[-118,2,0],"ti":[52.68,50.729,0]},{"t":283.000011526826,"s":[14.5,222.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":1,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-172.23,-7.099],[-161.73,29.401],[-73.23,-21.099]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[54.125,22.142],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":284.000011567557,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file