@papit/quaternion 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -2
- package/asset/translations/en.json +6 -0
- package/lib/bundle.d.ts +48 -0
- package/lib/bundle.js +1 -0
- package/package.json +77 -3
package/README.md
CHANGED
|
@@ -1,3 +1,138 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @papit/quaternion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Quaternion math for 3D rotations that compose and blend without gimbal lock. Reach for it when Euler angles fail in cameras, bones or animation.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
[](https://github.com/onkelhoy/papit/actions/workflows/pull-request.yml)
|
|
11
|
+
[](https://www.npmjs.com/package/@papit/quaternion)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @papit/quaternion
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { Quaternion } from "@papit/quaternion";
|
|
25
|
+
|
|
26
|
+
const turn = Quaternion.fromAxisAngle([0, 0, 1], Math.PI / 2);
|
|
27
|
+
turn.rotate([1, 0, 0]); // about [0, 1, 0]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Combine rotations, `b` runs first:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { Quaternion } from "@papit/quaternion";
|
|
34
|
+
|
|
35
|
+
const a = Quaternion.fromAxisAngle([0, 0, 1], .5);
|
|
36
|
+
const b = Quaternion.fromAxisAngle([1, 0, 0], .25);
|
|
37
|
+
|
|
38
|
+
const both = Quaternion.multiplyInto(Quaternion.identity, a, b);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Aim one direction at another:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { Quaternion } from "@papit/quaternion";
|
|
45
|
+
|
|
46
|
+
const aim = Quaternion.fromTo([1, 0, 0], [0, 3, 4]);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Blend two rotations and hand the result to a `Matrix4`:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { Quaternion } from "@papit/quaternion";
|
|
53
|
+
import { Matrix4 } from "@papit/matrix";
|
|
54
|
+
|
|
55
|
+
const from = Quaternion.identity;
|
|
56
|
+
const to = Quaternion.fromAxisAngle([0, 1, 0], 2);
|
|
57
|
+
|
|
58
|
+
const matrix = from.slerp(to, .5).writeMatrix(new Matrix4(), [10, 0, 0]);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
### Quaternion
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
class Quaternion extends Vector
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Stored as x, y, z, w in a `Float32Array`. Everything below is O(1). Instance methods change the quaternion and return it, except `rotate` and `writeMatrix`, which return their target.
|
|
70
|
+
|
|
71
|
+
#### Create
|
|
72
|
+
|
|
73
|
+
| Member | Purpose |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `Quaternion.identity` | A new quaternion with no rotation. |
|
|
76
|
+
| `Quaternion.fromAxisAngle(axis, angle)` | A turn in radians around an axis of any length. |
|
|
77
|
+
| `Quaternion.fromTo(from, to)` | The shortest rotation from one direction to another. |
|
|
78
|
+
| `identity()` | Resets to no rotation. |
|
|
79
|
+
| `setAxisAngle(x, y, z, angle)` | `fromAxisAngle` without allocating. |
|
|
80
|
+
| `setFromTo(from, to)` | `fromTo` without allocating. |
|
|
81
|
+
|
|
82
|
+
#### Combine
|
|
83
|
+
|
|
84
|
+
| Member | Purpose |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `Quaternion.multiplyInto(out, a, b)` | Writes `a * b`, which applies `b` first. `out` can be `a` or `b`. |
|
|
87
|
+
| `conjugate()` | Flips to the inverse, for a unit quaternion. |
|
|
88
|
+
| `normalize()` | Scales to length 1. Do this now and then after many products. |
|
|
89
|
+
| `rotate(point)` | Rotates a 3D point in place. |
|
|
90
|
+
|
|
91
|
+
#### Blend
|
|
92
|
+
|
|
93
|
+
| Member | Purpose |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `nlerp(target, t)` | Fast blend, the speed isn't constant over large angles. |
|
|
96
|
+
| `slerp(target, t)` | Blend at a constant speed. |
|
|
97
|
+
|
|
98
|
+
Both take the short way round and return a unit quaternion.
|
|
99
|
+
|
|
100
|
+
#### Output
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
writeMatrix(out: Float32Array, position?: ArrayLike<number>)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Writes the rotation and position into a column-major 4x4 matrix, such as a `Matrix4`.
|
|
107
|
+
|
|
108
|
+
### Degenerate input
|
|
109
|
+
|
|
110
|
+
| Input | Result |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| Zero axis or zero vector | The identity |
|
|
113
|
+
| Zero quaternion in `normalize` | The identity |
|
|
114
|
+
| Opposite directions in `fromTo` | A half turn around a perpendicular axis |
|
|
115
|
+
|
|
116
|
+
## Design notes
|
|
117
|
+
|
|
118
|
+
- `rotate`, `conjugate`, `nlerp`, `slerp` and `writeMatrix` expect a unit quaternion, which everything in Create gives you.
|
|
119
|
+
- `multiply` and `lerp` are inherited from `Vector` and work per component. Use `multiplyInto` to combine rotations and `nlerp` or `slerp` to blend them.
|
|
120
|
+
- Instance methods and `multiplyInto` don't allocate, so they're safe in a frame loop. The static `identity`, `fromAxisAngle` and `fromTo` create a new quaternion.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
Licensed under the @Papit License 1.0 - Copyright (c) 2024 Henry Pap (@onkelhoy)
|
|
125
|
+
|
|
126
|
+
**Key points:**
|
|
127
|
+
|
|
128
|
+
- ✅ Free to use in commercial projects
|
|
129
|
+
- ✅ Free to modify and distribute
|
|
130
|
+
- ✅ Attribution required
|
|
131
|
+
- ❌ Cannot resell the component itself as a standalone product
|
|
132
|
+
|
|
133
|
+
See the [LICENSE](https://github.com/onkelhoy/papit/blob/main/LICENSE) file for full details.
|
|
134
|
+
|
|
135
|
+
## Related
|
|
136
|
+
|
|
137
|
+
- [@papit/vector](https://github.com/onkelhoy/papit/tree/main/packages/algorithms/math/algebra/vector)
|
|
138
|
+
- [@papit/matrix](https://github.com/onkelhoy/papit/tree/main/packages/algorithms/math/algebra/matrix)
|
package/lib/bundle.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Vector } from '@papit/vector';
|
|
2
|
+
|
|
3
|
+
declare type Point = ArrayLike<number>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A 3D rotation stored as x, y, z, w, where a unit quaternion is
|
|
7
|
+
* `axis * sin(angle / 2), cos(angle / 2)`.
|
|
8
|
+
* Create one with `Quaternion.identity`, `fromAxisAngle` or `fromTo`.
|
|
9
|
+
* `multiply` and `lerp` are still Vector's component-wise ones,
|
|
10
|
+
* use `multiplyInto` to combine rotations and `nlerp` or `slerp` to blend them.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Quaternion extends Vector {
|
|
13
|
+
/** Sets this to no rotation. */
|
|
14
|
+
identity(): this;
|
|
15
|
+
/** Scales to length 1. A zero quaternion isn't a rotation, so it becomes the identity. */
|
|
16
|
+
normalize(): this;
|
|
17
|
+
/** Sets this to a turn of `angle` radians around an axis of any length. A zero axis gives the identity. */
|
|
18
|
+
setAxisAngle(x: number, y: number, z: number, angle: number): this;
|
|
19
|
+
/**
|
|
20
|
+
* Sets this to the shortest rotation that turns direction `from` onto direction `to`.
|
|
21
|
+
* A zero vector has no direction, so it gives the identity.
|
|
22
|
+
*/
|
|
23
|
+
setFromTo(from: Point, to: Point): this;
|
|
24
|
+
/** Flips this to its inverse, which undoes a unit quaternion. */
|
|
25
|
+
conjugate(): this;
|
|
26
|
+
/** Rotates a 3D point in place and returns it. This has to be a unit quaternion. */
|
|
27
|
+
rotate<T extends Float32Array | number[]>(point: T): T;
|
|
28
|
+
/** Moves this towards `target` the short way, normalized: 0 stays, 1 lands on it. Fast, a good fit for small steps. */
|
|
29
|
+
nlerp(target: Quaternion, t: number): this;
|
|
30
|
+
/** Moves this towards `target` the short way at a constant speed: 0 stays, 1 lands on it. */
|
|
31
|
+
slerp(target: Quaternion, t: number): this;
|
|
32
|
+
/** Writes this rotation and an optional position into a column-major 4x4 matrix. This has to be a unit quaternion. */
|
|
33
|
+
writeMatrix<T extends Float32Array>(out: T, position?: Point): T;
|
|
34
|
+
/**
|
|
35
|
+
* A half turn around the axis `x, y, z` leans on the least, made perpendicular to `x, y, z`.
|
|
36
|
+
* For a direction in a plane that's the plane's normal, so 2D stays 2D.
|
|
37
|
+
*/
|
|
38
|
+
private setHalfTurn;
|
|
39
|
+
/** Writes the Hamilton product `a * b`, which applies `b` first and then `a`. `out` can be `a` or `b`. */
|
|
40
|
+
static multiplyInto<T extends Float32Array>(out: T, a: Float32Array, b: Float32Array): T;
|
|
41
|
+
/** A turn of `angle` radians around an axis of any length. */
|
|
42
|
+
static fromAxisAngle(axis: Point, angle: number): Quaternion;
|
|
43
|
+
/** The shortest rotation that turns direction `from` onto direction `to`. */
|
|
44
|
+
static fromTo(from: Point, to: Point): Quaternion;
|
|
45
|
+
static get identity(): Quaternion;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { }
|
package/lib/bundle.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Vector as u}from"@papit/vector";var y=1e-6,d=.9995,m=class b extends u{identity(){return this[0]=0,this[1]=0,this[2]=0,this[3]=1,this}normalize(){let t=Math.hypot(this[0],this[1],this[2],this[3]);return t===0?this.identity():(this[0]/=t,this[1]/=t,this[2]/=t,this[3]/=t,this)}setAxisAngle(t,i,s,h){let n=Math.hypot(t,i,s);if(n===0)return this.identity();let r=Math.sin(h/2)/n;return this[0]=t*r,this[1]=i*r,this[2]=s*r,this[3]=Math.cos(h/2),this}setFromTo(t,i){let s=t[0],h=t[1],n=t[2],r=i[0],e=i[1],c=i[2],a=Math.hypot(s,h,n)*Math.hypot(r,e,c);if(a===0)return this.identity();let o=a+s*r+h*e+n*c;return o<y*a?this.setHalfTurn(s,h,n):(this[0]=h*c-n*e,this[1]=n*r-s*c,this[2]=s*e-h*r,this[3]=o,this.normalize())}conjugate(){return this[0]=-this[0],this[1]=-this[1],this[2]=-this[2],this}rotate(t){let i=this[0],s=this[1],h=this[2],n=this[3],r=t[0],e=t[1],c=t[2],a=2*(s*c-h*e),o=2*(h*r-i*c),l=2*(i*e-s*r);return t[0]=r+n*a+s*l-h*o,t[1]=e+n*o+h*a-i*l,t[2]=c+n*l+i*o-s*a,t}nlerp(t,i){let s=this.dot(t)<0?-1:1;return this[0]+=(t[0]*s-this[0])*i,this[1]+=(t[1]*s-this[1])*i,this[2]+=(t[2]*s-this[2])*i,this[3]+=(t[3]*s-this[3])*i,this.normalize()}slerp(t,i){let s=this.dot(t),h=s<0?-1:1,n=s*h;if(n>d)return this.nlerp(t,i);let r=Math.acos(n),e=Math.sin((1-i)*r),c=Math.sin(i*r)*h;return this[0]=this[0]*e+t[0]*c,this[1]=this[1]*e+t[1]*c,this[2]=this[2]*e+t[2]*c,this[3]=this[3]*e+t[3]*c,this.normalize()}writeMatrix(t,i){let s=this[0],h=this[1],n=this[2],r=this[3];return t[0]=1-2*(h*h+n*n),t[1]=2*(s*h+n*r),t[2]=2*(s*n-h*r),t[3]=0,t[4]=2*(s*h-n*r),t[5]=1-2*(s*s+n*n),t[6]=2*(h*n+s*r),t[7]=0,t[8]=2*(s*n+h*r),t[9]=2*(h*n-s*r),t[10]=1-2*(s*s+h*h),t[11]=0,t[12]=i?i[0]:0,t[13]=i?i[1]:0,t[14]=i?i[2]:0,t[15]=1,t}setHalfTurn(t,i,s){let h=Math.abs(t),n=Math.abs(i),r=Math.abs(s),e=(r<=h&&r<=n?s:n<=h?i:t)/(t*t+i*i+s*s);return this.setAxisAngle((r>h&&n>h?1:0)-t*e,(n<=h&&n<r?1:0)-i*e,(r<=h&&r<=n?1:0)-s*e,Math.PI)}static multiplyInto(t,i,s){let h=i[0],n=i[1],r=i[2],e=i[3],c=s[0],a=s[1],o=s[2],l=s[3];return t[0]=e*c+h*l+n*o-r*a,t[1]=e*a-h*o+n*l+r*c,t[2]=e*o+h*a-n*c+r*l,t[3]=e*l-h*c-n*a-r*o,t}static fromAxisAngle(t,i){return b.identity.setAxisAngle(t[0],t[1],t[2],i)}static fromTo(t,i){return b.identity.setFromTo(t,i)}static get identity(){return new b(0,0,0,1)}};export{m as Quaternion};
|
package/package.json
CHANGED
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papit/quaternion",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"description": "Quaternion math for 3D rotations that compose and blend without gimbal lock. Reach for it when Euler angles fail in cameras, bones or animation.",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"papit",
|
|
6
|
+
"quaternion",
|
|
7
|
+
"rotation",
|
|
8
|
+
"3d",
|
|
9
|
+
"math",
|
|
10
|
+
"game"
|
|
11
|
+
],
|
|
12
|
+
"version": "0.0.1",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "npx @papit/build -- --error --ancestors",
|
|
15
|
+
"watch": "tsc -w",
|
|
16
|
+
"start": "npm run build -- --dev && node ./lib/bundle.js",
|
|
17
|
+
"test": "node --test tests/**/*.test.js",
|
|
18
|
+
"test:only": "node --test --test-only tests/**/*.test.js",
|
|
19
|
+
"preversion": "papit-version",
|
|
20
|
+
"postversion": "papit-version",
|
|
21
|
+
"component": "npm create @papit -- --component"
|
|
22
|
+
},
|
|
23
|
+
"main": "./lib/bundle.js",
|
|
24
|
+
"types": "./lib/bundle.d.ts",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"files": [
|
|
27
|
+
"asset/",
|
|
28
|
+
"lib/"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"import": "./lib/bundle.js",
|
|
33
|
+
"types": "./lib/bundle.d.ts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"author": "henry",
|
|
37
|
+
"private": false,
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"license": "LicenseRef-Papit-1.0",
|
|
43
|
+
"homepage": "https://github.com/onkelhoy/papit/tree/main/packages/algorithms/math/algebra/quaternion",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/onkelhoy/papit/issues"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/onkelhoy/papit.git",
|
|
50
|
+
"directory": "packages/algorithms/math/algebra/quaternion"
|
|
51
|
+
},
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@papit/vector": "0.0.2"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@papit/matrix": "0.0.2",
|
|
61
|
+
"@types/node": "22.10.2",
|
|
62
|
+
"typescript": "5.9.3"
|
|
63
|
+
},
|
|
64
|
+
"browserslist": [
|
|
65
|
+
"defaults",
|
|
66
|
+
"not IE 11",
|
|
67
|
+
"not dead"
|
|
68
|
+
],
|
|
69
|
+
"papit": {
|
|
70
|
+
"publish": true,
|
|
71
|
+
"type": "node",
|
|
72
|
+
"main": "quaternion",
|
|
73
|
+
"components": {
|
|
74
|
+
"quaternion": {
|
|
75
|
+
"className": "Quaternion",
|
|
76
|
+
"htmlprefix": "pap"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
6
80
|
}
|