How can I customise the Animation of an Angle change in SwiftUI - swiftui

I have an app that shows a bunch of people who each have an origin and angle.
struct Location {
var centre:CGPoint
var facing:Angle
}
SwiftUI magically and automatically does a lot of the animation as they move from location A to location B
withAnimation {
person.location = newLocation
}
However - for the Angle (facing) property, I want the animation to go in the shortest route (bearing in mind that in the real world - angles wrap around).
e.g. Swift UI correctly animates when the angle changes 5 -> 10 (degrees)
5,6,7,8,9,10
but going from 2 to 358, it takes the long way around
SwiftUI does 2,3,4,5,6,7.......,357,358
where I would like it to do
2,1,0,359,358
how can I go about this?
thank you
update: I'm hoping for a solution which allows me to work with the animation system, perhaps using a new MyAngle struct which provides the animation steps directly, perhaps using some kind of animation modifier.
.easeInOut modifies the steps - is there an equivalent approach where I can create a .goTheRightWay animation?

Ok - Posting my own answer.
It works a bit like #Ben's answer - but moves the 'shadow angle' management to the rotation effect.
All you have to do is switch rotationEffect(angle:Angle) for shortRotationEffect(angle:Angle,id:UUID)
this looks like
#State private var rotationStorage = RotationStorage()
//and then in body
Image(systemName: "person.fill").resizable()
.frame(width: 50, height: 50)
.shortRotationEffect(self.person.angle,id:person.id,storage:rotationStorage)
.animation(.easeInOut)
the ShortRotationEffect uses the provided id to maintain a dictionary of previous angles. When you set a new angle, it figures out the equivalent angle which provides a short rotation and applies that with a normal rotationEffect(...)
Here it is:
class RotationStorage {
private var storage: [UUID: Angle] = [:]
fileprivate func setAngle(id:UUID,angle:Angle) {
storage[id] = angle
}
fileprivate func getAngle(_ id:UUID) -> Angle? {
return storage[id]
}
}
extension View {
/// Like RotationEffect - but when animated, the rotation moves in the shortest direction.
/// - Parameters:
/// - angle: new angle
/// - anchor: anchor point
/// - id: unique id for the item being displayed. This is used as a key to maintain the rotation history and figure out the right direction to move
func shortRotationEffect(_ angle: Angle,
anchor: UnitPoint = .center,
id: UUID,
storage:RotationStorage) -> some View {
modifier(ShortRotation(angle: angle,
anchor: anchor,
id: id,
storage:storage))
}
}
struct ShortRotation: ViewModifier {
var angle: Angle
var anchor: UnitPoint
var id: UUID
let storage:RotationStorage
func getAngle() -> Angle {
var newAngle = angle
if let lastAngle = storage.getAngle(id) {
let change: Double = (newAngle.degrees - lastAngle.degrees) %% 360.double
if change < 180 {
newAngle = lastAngle + Angle.init(degrees: change)
} else {
newAngle = lastAngle + Angle.init(degrees: change - 360)
}
}
storage.setAngle(id: id, angle: newAngle)
return newAngle
}
func body(content: Content) -> some View {
content
.rotationEffect(getAngle(), anchor: anchor)
}
}
this relies on my positive modulus function:
public extension Double {
/// Returns modulus, but forces it to be positive
/// - Parameters:
/// - left: number
/// - right: modulus
/// - Returns: positive modulus
static func %% (_ left: Double, _ right: Double) -> Double {
let truncatingRemainder = left.truncatingRemainder(dividingBy: right)
return truncatingRemainder >= 0 ? truncatingRemainder : truncatingRemainder+abs(right)
}
}

How about adjusting the newLocation value to keep within 180˚ of the start? Here's a function to check if the distance animated is greater than half way around and provide a new endpoint that satisfies it.
func adjustedEnd(from start: CGFloat, to target: CGFloat) -> CGFloat {
// Shift end to be greater than start
var end = target
while end < start { end += 360 }
// Mod the distance with 360, shifting by 180 to keep on the same side of a circle
return (end - start + 180).truncatingRemainder(dividingBy: 360) - 180 + start
}
Some sample test cases:
let startValues: [CGFloat] = [2, -10, 345, 365, 700]
let endValues: [CGFloat] = [2, 10, 180, 185, 350, -10, 715, -700]
for start in startValues {
print("From \(start):")
for end in endValues {
let adjusted = adjustedEnd(from: start, to: end)
print("\t\(end) \tbecomes \(adjusted);\tdistance \(abs(adjusted - start))")
}
}
prints the following:
From 2.0:
2.0 becomes 2.0; distance 0.0
10.0 becomes 10.0; distance 8.0
180.0 becomes 180.0; distance 178.0
185.0 becomes -175.0; distance 177.0
350.0 becomes -10.0; distance 12.0
-10.0 becomes -10.0; distance 12.0
715.0 becomes -5.0; distance 7.0
-700.0 becomes 20.0; distance 18.0
From -10.0:
2.0 becomes 2.0; distance 12.0
10.0 becomes 10.0; distance 20.0
180.0 becomes -180.0; distance 170.0
185.0 becomes -175.0; distance 165.0
350.0 becomes -10.0; distance 0.0
-10.0 becomes -10.0; distance 0.0
715.0 becomes -5.0; distance 5.0
-700.0 becomes 20.0; distance 30.0
From 345.0:
2.0 becomes 362.0; distance 17.0
10.0 becomes 370.0; distance 25.0
180.0 becomes 180.0; distance 165.0
185.0 becomes 185.0; distance 160.0
350.0 becomes 350.0; distance 5.0
-10.0 becomes 350.0; distance 5.0
715.0 becomes 355.0; distance 10.0
-700.0 becomes 380.0; distance 35.0
From 365.0:
2.0 becomes 362.0; distance 3.0
10.0 becomes 370.0; distance 5.0
180.0 becomes 540.0; distance 175.0
185.0 becomes 185.0; distance 180.0
350.0 becomes 350.0; distance 15.0
-10.0 becomes 350.0; distance 15.0
715.0 becomes 355.0; distance 10.0
-700.0 becomes 380.0; distance 15.0
From 700.0:
2.0 becomes 722.0; distance 22.0
10.0 becomes 730.0; distance 30.0
180.0 becomes 540.0; distance 160.0
185.0 becomes 545.0; distance 155.0
350.0 becomes 710.0; distance 10.0
-10.0 becomes 710.0; distance 10.0
715.0 becomes 715.0; distance 15.0
-700.0 becomes 740.0; distance 40.0
(Edited to account for negative ending values)
Edit: From your comment about keeping a second value around, what about setting Location.facing to the adjusted angle, and then adding to Location something like
var prettyFacing: Angle {
var facing = self.facing
while facing.degrees < 0 { facing += Angle(degrees: 360) }
while facing.degrees > 360 { facing -= Angle(degrees: 360) }
return facing
}

After trying both of the other options, we were still getting visual glitches (less common, but still there!).
Our Solution: Use UIKit for Animation
We've created a SPM package that adds a simple modifier, .uiRotationEffect(). This modifier wraps your View in a UIView, and uses UIView's .animate(...) function to get the correct behavior.
You can install the package here or you can just copy and paste the code here, it's not very long.
GIF of the working solution:

Related

2D rotation of a quad

I am working on a basic simulation program using C++ and I have a renderer that uses OpenGL. I am rendering quads on the screen which have a dynamic location in the simulation. My goal is to change the orientation of the quad when it is moving in the simulation. For each quad, I have a variable (m_Rotation) which holds the current rotation of it and I calculate the required rotation using trigonometry and put the value in a variable (m_ProjectedRotation). In the render loop, I use the following code to change the orientation in the movement:
if(abs(m_ProjectedRotation - m_Rotation)>5.0f)
{
if ((360.0f - m_Rotation + m_ProjectedRotation) > (m_ProjectedRotation - m_Rotation))
{
m_Rotation += 5.0f;
if (m_Rotation > 360)
{
m_Rotation = fmod(m_Rotation, 360);
}
}
else
{
m_Rotation -= 5.0f;
}
}
I want the quad to rotate itself according to the closest angle(e.g. if the current angle is 330, and the destination angle is 30, the quad should increase its angle until it reaches 30 not decreasing the angle since it reaches 30. Because it has a smaller angle to rotate). In some conditions, my quad rotates itself counterclockwise even tough the clockwise rotation has shorter rotation and vice versa. I believe the condition for rotation:
(360.0f - m_Rotation + m_ProjectedRotation) > (m_ProjectedRotation - m_Rotation)
should be something different to show the required behavior. However, I couldn't figure it out. How should I update this code to get what I want?
I believe the correct solution should be as follows:
Let's call the two angles from and to. I assume both are in positive degrees as per your question. There are two cases:
the absolute distance |to - from| is less than 180.
This means there is less degrees to travel by to - from than in the other direction, and that is the way you should choose.
In this case, you should rotate by sign(to-from) * deltaRotation, where sign(x) = 1 if x > 0 and -1 otherwise. To see the need of the sign function, look at the following 2 examples, where |to - from| < 180:
from = 10, to = 20. to - from = 10 > 0, so you should increase rotation.
from = 20, to = 10. to - from = -10 < 0, you should decrease rotation.
|to - from| is more than 180. In this case, the direction should be the inverse, and you should rotate by - sign(to-form) * deltaRotation, note the minus sign. You could also express this as sign(from-to) * deltaRotation, swapping from and to, but I left them as before for explicitness.
from = 310, to = 10. Then, to - from = -300 < 0, you should increase rotation (Formally, -sign(to-from) = -sign(-300) = -(-1) = 1)
from = 10, to = 310. Then, to - from = 300 > 0, you should decrease rotation (Formally, -sign(to-from) = -sign(300) = -1)
Writing this in C++ you can encapsulate this logic in such a function:
int shorterRotationSign(float from, float to) {
if(fabs(to - from) <= 180) {
return to - from > 0 ? 1 : -1;
}
return to - from > 0 ? -1 : 1;
}
Which you will use like this:
m_Rotation += 5.0f * shorterRotationSign(m_Rotation, m_ProjectedRotation);
m_Rotation = fmod(m_Rotation + 360, 360);
The goal of the last line is to normalize negative angles and ones greater than 360.
(IMO This is more of a mathematical question than one about opengl.)
I would do something like that:
auto diff = abs(m_ProjectedRotation - m_Rotation);
if(diff > 5.0f)
{
diff = fmod(diff, 360.0f);
auto should_rotate_forward = (diff > 180.0f) ^ (m_ProjectedRotation > m_Rotation);
auto offset = 360.0f + 5.0f * (should_rotate_forward ? 1.0f : -1.0f);
m_Rotation = fmod(m_Rotation + offset, 360.0f);
}
diff is the absolute angle of your rotation. You then make it in a [0; 360) range by doing diff = fmod(diff, 360.0f).
should_rotate_forward determines whether you should decrease or increase the current angle. Note the ^ is a XOR operation
offset is basically either -5.0 or 5.0 depending on condition, but there's also +360.0f so that if for example m_Rotation == 1.0 and offset == -5.0 so fmod(m_Rotation + offset, 360.0f) would be -4.0 while you want 356.0, so you add full 360 rotation and after fmod everything is positive and in [0; 360) range

Return a Value between 1 & 360

I have a function set to return a Value being used in a Quaternin Rotation. Howver when I have a reslt less than 1 or very close to 360 I recieve a result of -nan(ind). Is there a way to return a conclusive result betwen 1 & 360 to avoid any errors in calculaon. Thanks
const float ReturnAngle() const
{
return (acosf(angle) * 180.0f * 2) / PI;
}
Update:
These are some results as to wat I receive when checking the value after applying a transformaion rotation to my quatrnion.
angle: 359.598
angle: -nan(ind)
angle: 357.843
angle: -nan(ind)
angle: -nan(ind)
angle: 0.798189
angle: 2.16383
angle: 1.75475
angle: -nan(ind)
The value that you are passing to acosf is outside the allowed range -1 to 1. Review the code that sets the value for angle - perhaps you have a bug there. If it is just a rounding error and angle is outside the allowed range by a small value, "clamping" it to the valid range may be a good solution.
const float ReturnAngle() const
{
if (angle < -1f) angle = -1f;
if (angle > +1f) angle = +1f;
return (acosf(angle) * 180.0f * 2) / PI;
}

how to use Element.status(); in Raphael.js

the documentation is too breif.
can anyone knowa about how to use Element.status() in Raphael.js?
very appreciate if you can give a demo.
Element.status shows the current animation applied to an element OR you can set the status on a particular animation.
Think of an animation interpolating between 0 and 1. So half way through an animation, it would have a value of 0.5. This is applied to whatever attribute is being animated. (So if animating x from 0 to 100 half way through, status will be 0.5 and x will be 50, not accounting for any easing applied like bounce).
So lets suppose instead of creating a nice smooth interpolation between attributes, we want to make an element move in 10 discrete steps. We can set the status each time of an animation manually. Example.
var raphAnimation = Raphael.animation( { x: 100, y: 100 }, 1000);
rect.status( raphAnimation, 0.5 )
example jsfiddle
var rect = paper.rect(10, 20, 300, 200);
var raphAnimation = Raphael.animation( { x: 100, y: 100 }, 1000);
for( var c = 1; c <= 10; c++ ) {
(function() {
var step = c
setTimeout( function() {
rect.status( raphAnimation, 0.1 * step )
}, step * 200)
})();
}
Note: If you repeat using the status set command, I think it will take the animation as fresh. So suppose the animation is from 0->100 and you set status to be 0.5 it will go half way. Now suppose you do the same and call it again with 0.5, it will now be 0.5 of the 'remainder' as it now only has half the distance to traverse.

Swift Double is Not Convertible to CGFloat

I'm trying to draw a simple circle when I get to the following line I get the error "Double is Not Convertable to CGFloat under the startAngle = 0.0
path.addArcWithCenter(center, radius: radius, startAngle: 0.0, endAngle: Float(M_PI) * 2.0, clockwise: true)
How do I "cast" 0.0 to make it CGFloat in Swift?
The complete function I am writing:
func drawCircle() {
// Drawing code
var bounds:CGRect = secondView.bounds
var center = CGPoint()
center.x = bounds.origin.x + bounds.size.width / 2.0
center.y = bounds.origin.y + bounds.size.height / 2.0
var radius = (min(bounds.size.width, bounds.size.height) / 2.0)
var path:UIBezierPath = UIBezierPath()
path.addArcWithCenter(center, radius: radius, startAngle: CGFloat(0.0), endAngle: Float(M_PI) * 2.0, clockwise: true)
path.stroke()
}
Convert the values that need to be CGFloat to a CGFloat.
path.addArcWithCenter(center, radius: CGFloat(radius), startAngle: CGFloat(0.0), endAngle: CGFloat(M_PI) * 2.0, clockwise: true)
startAngle probably shouldn't need to be converted though if you're just passing a literal. Also note that this isn't a C style cast, but actually converting between different Swift Types.
Edit: Looking at your whole function, this works.
func drawCircle() {
// Drawing code
var bounds:CGRect = self.view.bounds
var center = CGPoint()
center.x = bounds.origin.x + bounds.size.width / 2.0
center.y = bounds.origin.y + bounds.size.height / 2.0
var radius = (min(bounds.size.width, bounds.size.height) / 2.0)
var path:UIBezierPath = UIBezierPath()
path.addArcWithCenter(center, radius: CGFloat(radius), startAngle: CGFloat(0.0), endAngle: CGFloat(Float(M_PI) * 2.0), clockwise: true)
path.stroke()
}
You must type cast it via CGFloat(0.0). CGFloat has been adjusted to evaluate differently throughout the beta version of Xcode 6 due to the fact that in Obj-C, CGFloat casts to either a float or a double depending on the target (64 bit versus 32 bit). You must type cast a number to CGFloat in Swift to use a CGFloat as you're never guaranteed to have a float or a double (because this is dependent on the environment). This way, Swift won't throw a fit and will still be 'type' safe.
This error will disappear in Swift 5.5.
Maybe it's not a good idea, but I used NSNumber to convert Double to Float, then to CGFloat.
let myFloat = NSNumber.init(value: myDouble).floatValue
let myCGFloat = CGFloat(myFloat)

how to get UIView 's angle after CGAffineTransform?

i have been do more CGAffineTransform.
how to get current Uiview's angle?
i want to do something when angle==[M_PI/2 ~ M_PI]?
CGFloat radians = atan2f(self.view.transform.b, self.view.transform.a);
CGFloat degrees = radians * (180 / M_PI);
You can check .transfrom property of UIView for that.