Repository: bitshifter/glam-rs
Pull request #286 (2022-05-04 04:45:01)

added rotate
Wondering if we should name it something else, like `turn`, since it encompasses this weird rotate-multiply operation as well if `self` isn't normalized, or in case you wanted there to also be a rotate method that normalizes self automatically. But this would be my preference.

bitshifter: What is the use case for this? I feel like perhaps the method name could be more descriptive.
makoConstruct: I'm not sure how important it is to have this, but I like it because a normal is usually the most efficient thing for 2d rotation or for projecting or antiprojecting. Usually, if I'm messing with angles, I want them in the form of a normal.
But not always.
I'm not sure about this, but it's possible they're more efficient for rotations than rotation-only transforms, since transforms involve an add, which might not always be optimized out?
makoConstruct: I'm not sure I have a use case for rotate by a non-normalized vec2, but I've never looked for one. I'm reluctant to forbid it.
bitshifter: Can you give an example of usage? It's not something I've done before.
makoConstruct: I guess this is a really good recent example from the wall physics of a friend's game (`component(a, b)` is `b.project(a)` by the way)

The angles of the walls are communicated as vec2s, which lets me do a number of things with them really efficiently.

```typescript
//bounce off the walls
//the walls are infinitely long btw, p is a point on it, dir is the direction the wall is facing
let nextPosition = this.pos.add((this.acc.scale(deltasec).add(this.vel)).scale(deltasec))
let applyWall = (p: Vector, dir: Vector) => {
    if (dir.dot(nextPosition.add(p.negate())) < 0) {
        // move it to the wall
        this.pos = this.pos.add(component(this.pos.add(p.negate()), dir).negate())
        let vc = component(this.vel, dir.negate())
        this.vel = this.vel.add(vc.negate().scale(1 + 1 - conf.wallBounceAbsorb))
        this.acc = removeComponent(this.acc, dir.negate())
    }
}
applyWall(vec(0, 0), vec(1, 0))
applyWall(vec(0, 0), vec(0, 1))
applyWall(vec(conf.arenaWidth, conf.arenaHeight), vec(-1, 0))
applyWall(vec(conf.arenaWidth, conf.arenaHeight), vec(0, -1))

let applyCircularWall = (p: Vector, radius: number)=> {
    if(nextPosition.sub(p).size > radius){
       // move it to the wall
       this.pos = p.add(sized(this.pos.sub(p), radius))
       let dir = nextPosition.sub(p).normalize().negate()
       let vc = component(this.vel, dir.negate())
       this.vel = this.vel.add(vc.negate().scale(1 + 1 - conf.wallBounceAbsorb))
       this.acc = removeComponent(this.acc, dir.negate())
    }
}
applyCircularWall(vec(conf.arenaWidth/2, conf.arenaHeight/2), min(conf.arenaWidth, conf.arenaHeight)/2)
```
makoConstruct: Wait, sorry, that's not what component is. it gets the length of `b.project(a)`.
Well. Hopefully you can tell what it's doing even if I haven't made it clear how.
bitshifter: Are you aware of a proof of this technique or it's existence in another math library? It's not familiar to me, so I want to understand how it works before adding it to glam.

> Returns `other` rotated by the angle of `self`. If `self` is normalized,

I assume by this, the angle is the angle between the vector `self` and some assumed unit vector like (1, 0) or (0, 1)?
makoConstruct: I'm not. I just scrawled it up for my own engine and have been using it a lot.
It's a rotation relative to (1, 0).
Here's a proof, maybe: Take the components of the vec you want to rotate. Imagine multiplying the normal by the x, then adding the result of multiplying the perp of the normal by the y. Naturally this gets you the rotation of (x, y). And that's all the formula is. I don't really know how to define rotation in more essential terms than this so that's all I've got.
And if the rotator isn't normalized, it's just that rotation with the whole thing multiplied by a scale factor.

To complete the API, you might also want `fn normal_from_angle(v:f32)-> Self { Self::new(cos(v), sin(v)) }`
makoConstruct: Could call it "scotate" I guess (scale-rotate)
bitshifter: OK, I think I get it. This is effectively shortcutting matrix rotation as a 2D rotation matrix is created from angle `a`.

```
| cos(a), -sin(a) |
| sin(a),  cos(a) |
```

multiplying a `Vec2` by `Mat2` is

```
(((m11 * x) + (m21 * y)), ((m12 * x) + (m22 * y)))
```

substituting the rotation matrix gives

```
(((cos(a) * x) + (-sin(a) * y)), ((sin(a) * x) + (cos(a) * y)))
```

Which is what your `rotate` method is doing.

So for an angle of 0 you would get a `Vec2` of (1, 0) or `Vec2::X` and then I guess angle of rotation is the angle between `self` and `Vec2::X`.

Patch:
diff --git a/src/vec2.rs b/src/vec2.rs
index 98ffa38..4a666f0 100644
--- a/src/vec2.rs
+++ b/src/vec2.rs
@@ -67,6 +67,18 @@ macro_rules! impl_vec2_signed_methods {
         pub fn perp_dot(self, other: $vec2) -> $t {
             self.0.perp_dot(other.0)
         }
+
+        /// Returns `other` rotated by the angle of `self`. If `self` is normalized,
+        /// then this just rotation. This is what you usually want. Otherwise,
+        /// it will be like a rotation with a multiplication by `self`'s length.
+        #[must_use]
+        #[inline(always)]
+        pub fn rotate(self, other: Self) -> Self {
+            Self::new(
+                self.x * other.x - self.y * other.y,
+                self.y * other.x + self.x * other.y,
+            )
+        }
     };
 }
 


Test patch:
diff --git a/tests/vec2.rs b/tests/vec2.rs
index 079f16b..c9b39fd 100644
--- a/tests/vec2.rs
+++ b/tests/vec2.rs
@@ -672,6 +672,13 @@ macro_rules! impl_vec2_float_tests {
                 $vec2::new(-0.5, 1.0)
             );
         });
+
+        glam_test!(test_rotate, {
+            assert_eq!(
+                $vec2::new(0.0, 1.0).rotate($vec2::new(1.0, 1.0)),
+                $vec2::new(-1.0, 1.0)
+            );
+        });
     };
 }