rematrix
rematrix copied to clipboard
Apply Transforms to Vectors
It's not hard to do such a function (and I will do one for my personnal usage), but why is there no function to apply the matrix to a point ? (would be convenient convert coordinate page to element coordinate ?)
...Or at least a column vector (just to set z to 0 and w to 1)
I'm not sure I understand what you mean, but I'd be happy to take a look at what you come up with.
Some functions like these (please rename them, because I was definitively not inspired xD )
/**
* Apply the matrix to a 2D point, and returns a point
* @param {array} m - Matrix
* @param {object} p - 2D point (with x and y attributes)
* @return {object}
*/
function matAppPt2D(m, p) {
const t = m[3] * p.x + m[7] * p.y + m[15]
return {
x : (m[0] * p.x + m[4] * p.y + m[12]) / t,
y : (m[1] * p.x + m[5] * p.y + m[13]) / t,
}
}
/**
* Apply the matrix to a 3D point, and returns a point
* @param {array} m - Matrix
* @param {object} p - 3D point (with x, y and z attributes)
* @return {object}
*/
function matAppPt3D(m, p) {
const t = m[3] * p.x + m[7] * p.y + m[11] * p.z + m[15]
return {
x : (m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12]) / t,
y : (m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13]) / t,
z : (m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]) / t,
}
}
... And you could also rework the multiply function by first removing the row and col arrays (small optimization), and taking an arbitrary number of columns. That would permit :
- conserve the current behavior
- support multiply (apply to) with a column vector
- compute a batch of column vectors (points) at once.
I like your library for its simplicity and its ease of use for the css-transform property, and I think these proposed addition could only improve it !
it's affine matrix math :
in 2D, the "simple" case is to use 2×2 matrices. But they can only express rotation and scales (in both axis) (but I think you know that). The trick to support translation, it's to add another column to the matrix, where you tell the amount of translation (the tx
and ty
parameters in css matrix()
function).
so you have a 2×3 matrix. And to apply this matrix to a (2D) point, you need 3 coordinates. This is where this "t" comes from.
And here, we get into the projective space : instead of considering a point to have a unique coordinate, we consider a point to have a family of coordinates, that are
(it is called the "projective" space, because it's the equation of the pin-hole camera model)
Then, you establish the convention that the normalized coordinate for a point (the coordinate you want on the screen) is the only vector in the family such that t
is 1. (1 being chosen for it's neutral role in scalar multiplication). Follows that to get normalized coordinates for a point, you have to divide it by t at end. (even if since it's for css, the resulting t
should always be 1, I thinks it's a good practice to always do it...)
...And the same apply to 3D transformations.
Thank you for the explanation.
The variable t
would always be 1
due to all the zeroes, i.e: (0 * p.x) + (0 * p.y) + (0 * p.z) + 1
, which I found a little confusing in your algorithm. I'd be curious to learn more about when t
is not 1
.
I can imagine why you might want to transform a vector generally speaking—but I'm skeptical about making Rematrix a more general purpose math library. It was designed to mimic CSS Transforms for everyday web developers, and I'm worried that a more robust linear algebra API would also be more exclusive.
Do you think I'm overlooking an important use case for web developers?
Indeed, I think t
would never be different than 1
in affine transformation (since it's a group), I guess, it was some kind of defensive programming since I did some stuff using projections and I didn't asked myself if it was relevant here ^^
Though, the need to apply a matrix to a vector still makes sense for translating page coordinates to local element's coordinate. For example, to drag and drop an object in a transformed surface (this is actually my current use case for such a function).
The mouse events don't give you any tools to get the coordinate of the event in the container local coordinate system (offsetX and offsetY being in event's target's local coordinate system only... that could change). Thus, you have to do it your self.
In my specific case, the code looks like this:
function translateCoordinates(p) {
const style = getComputedStyle(transformed_container).transform;
const box = parent_of_transformed_container.getBoundingClientRect()
let m = Rematrix.multiply(
Rematrix.inverse(Rematrix.parse(style)),
Rematrix.translate(-(box.x + window.pageXOffset), -(box.y + window.pageYOffset))
)
return matAppPt2D(m, p) // here, the function could be part of Rematrix
}
This looks interesting @hl037, I wonder if a blog post could be written about it! 😬 #7 suggests more examples would be nice, so I'd like to better understand what you're doing here.
I may write some post some time :D The ultimate goal would be to make a function to translate coordinates from any elements to any elements, but I don't need one for now.
currently, I implemented a rubber-band selection tool, and I have to track the mouse move events to move the handles (like in MS Word when you change an image position/scale). When the surface on which the rubber-band acts is css-transformed, 3 major problems occurs:
-
If you want the to place a rubber-band on an area (a div) which is css-transformed (like a zoom, or a 3D rotation), you have to compute the local
top
andleft
css properties. on the first click event, it's easy : just useev.offsetX
. 1.1) But, when you click on a handle,offsetX
is relative to the handle, not the parent node. And if the handle is itself css-transformed, or the support area got a perspective transformation, then you have incoherent coordinates. 1.2) Even if there are no css-transformations on the handles nor perspective on the support area, you get a first point with coordinates local to the handle (not the area). When you move your mouse, chances are the cursor will leave the handle's area (if you move your mouse fast, you can have a delta > 20px, and if the handle is 10px [which is classical], the cursor gets outside), and then the event target will be the parent area, and once again, you get incoherent coordinates compared to the first ones you got, and can't modify the rubber-band size following the user cursor. -
Surprisingly there are no function in js std libs to get the mouse event locally to the listening node. You always get the coordinates local to the target node
-
Less surprisingly, no reliable functions exists to translate any screen nor page coordinates to any element's local coordinates.
...This is why I should use matrices, and why I use you library :)
I don't need advanced linear algebra functions, just something that can interact nicely with css, invert matrices, and project points.
This way, I compute the total transform matrix for my area (supposing the area is static, else, I would do caches similar in what we find in 3D render engines), and I use ev.pageX
and ev.pageY
.
in the snippet above, I used getBoundingClientRect()
to get the translation of the support area since my area's parent is not transformed (so, it's the fastest and most accurate method to get it), but you have to correct it with the scroll offsets then...
Not yet publishing this, but wanted to archive some experimental code.
Type Declaration
export declare interface Vector2 {
x: number;
y: number;
}
export declare interface Vector3 {
x: number;
y: number;
z: number;
}
export declare type Matrix2D = [
number, number,
number, number,
number, number,
];
export declare type Matrix3D = [
number, number, number, number,
number, number, number, number,
number, number, number, number,
number, number, number, number,
];
export declare type Matrix = Matrix2D | Matrix3D;
/**
* Apply a transformation matrix to a point.
* @param matrix A `number[]` with length 6 or 16.
* @param vector An `object` with `x` and `y` properties.
*/
export declare function transformVector2(matrix: Matrix, vector: Vector2): Vector2
/**
* Apply a transformation matrix to a point.
* @param matrix A `number[]` with length 6 or 16.
* @param vector An `object` with `x`, `y` and `z` properties.
*/
export declare function transformVector3(matrix: Matrix, vector: Vector3): Vector3
Implementation
export function transformVector2(matrix, vector) {
let fm = format(matrix)
if ('x' in vector && 'y' in vector) {
let t = fm[3] * vector.x + fm[7] * vector.y + fm[15]
return Object.assign({}, vector, {
x: (fm[0] * vector.x + fm[4] * vector.y + fm[12]) / t,
y: (fm[1] * vector.x + fm[5] * vector.y + fm[13]) / t,
})
}
throw new TypeError('Expected object with `x` and `y` properties.')
}
export function transformVector3(matrix, vector) {
let fm = format(matrix)
if ('x' in vector && 'y' in vector && 'z' in vector) {
let t = fm[3] * vector.x + fm[7] * vector.y + fm[11] * vector.z + fm[15]
return Object.assign({}, vector, {
x: (fm[0] * vector.x + fm[4] * vector.y + fm[8] * vector.z + fm[12]) / t,
y: (fm[1] * vector.x + fm[5] * vector.y + fm[9] * vector.z + fm[13]) / t,
z: (fm[2] * vector.x + fm[6] * vector.y + fm[10] * vector.z + fm[14]) / t,
})
}
throw new TypeError('Expected object with `x`, `y` and `z` properties.')
}
Note: Use of
Object.assign()
breaks IE10 and IE11 support.
Would appreciate this, +1!
The ultimate goal would be to make a function to translate coordinates from any elements to any elements, but I don't need one for now.
After 4 years and some nice js api changes, I'm pleased to announce I implemented and polished a library to do exactly that !
It handles all situations I could have tested so far, and since it relies on rematrix
, I though it would be kind to tell you =)
--> https://github.com/hl037/quantpos
Cool @hl037 I'll check it out this week!