I'm learning linear algebra and trying to make a little program with basic linear transformations (rotating, scaling, translating).
https://codesandbox.io/embed/determined-diffie-t2iy5?fontsize=14&hidenavigation=1&theme=dark
I wrote functions for generating each matrix for each transformation and functions to calculate them (multiplying matrix with a point, multiplying matrices).
export const multiplyMatrixWithPoint = (matrix, point) => {
return point.map((dimension, index) => {
let result = 0;
for (let i = 0; i < 4; i++) {
const matrixIndex = index * 4 + i;
result += dimension * matrix[matrixIndex];
}
return +result.toFixed(2);
})
};
// Just creating 2D array to make it easy to calculate the matrix
export const matrixToPoints = matrix => {
const result = [];
for (let i = 0; i < 4; i++) {
const onePoint = [];
for (let j = 0; j < 4; j++) {
onePoint.push(matrix[4 * i + j]);
}
result.push(onePoint);
}
return result;
};
// Just making 2D array to 1D
export const pointsToMatrix = points => points.reduce((acc, points) => [...acc, ...points], []);
// Transpose function to make the matrix multiplication correct
export const transposeCSSMatrixToTransform = matrix => matrix[0].map((column, index) => matrix.map(row => row[index]));
export const multiplyMatrices = (matrixA, matrixB) => {
const separatePoints = transposeCSSMatrixToTransform(matrixToPoints(matrixB));
return pointsToMatrix(transposeCSSMatrixToTransform(separatePoints.map(point => multiplyMatrixWithPoint(matrixA, point))));
};
export const rotationMatrixGenerator = (matrix, angle) => {
const radians = degreeToRadian(angle);
matrix[0] = Math.cos(radians);
matrix[1] = -Math.sin(radians);
matrix[4] = Math.sin(radians);
matrix[5] = Math.cos(radians);
return matrix;
};
export const scaleMatrixGenerator = (matrix, { x, y }) => {
matrix[0] = x;
matrix[5] = y;
return matrix;
};
export const translateMatrixGenerator = (matrix, { x, y }) => {
matrix[12] = x;
matrix[13] = y;
return matrix;
};
For showing the transformations I used CSS's matrix3d
export const matrixToCSSMatrix = matrix => `matrix3d(${matrix.join(',')})`;
And here are the executing part of the code
...
const [matrix, setMatrix] = useState(DEFAULT_MATRIX);
const [rotationAngle, setRotationAngle] = useState(0);
const [scale, setScale] = useState(DEFAULT_XY);
const [translate, setTranslate] = useState(DEFAULT_XY);
const classes = useStyles();
const { app } = classes;
// ---------------------- Rotate --------------------- //
const rotate = useCallback(e => {
const { value: angle } = e.target;
const rotationMatrix = rotationMatrixGenerator(matrix, +angle);
const updatedMatrix = multiplyMatrices(matrix, rotationMatrix);
setMatrix(updatedMatrix);
setRotationAngle(angle);
}, [matrix]);
// ---------------------- Scale --------------------- //
const changeScale = useCallback((e, dimension) => {
const { value } = e.target;
const updatedScale = {
...scale,
[dimension]: +value
};
const scaleMatrix = scaleMatrixGenerator(matrix, updatedScale);
const updatedMatrix = multiplyMatrices(matrix, scaleMatrix);
setMatrix(scaleMatrix);
setScale(updatedScale);
}, [matrix, scale]);
// ------------------ Translate --------------------- //
const changeTranslation = useCallback((e, position) => {
const { value } = e.target;
const updatedTranslation = {
...translate,
[position]: +value
};
const translateMatrix = translateMatrixGenerator(matrix, updatedTranslation);
const updatedMatrix = multiplyMatrices(matrix, translateMatrix);
setMatrix(translateMatrix);
setTranslate(updatedTranslation);
}, [matrix, translate]);
...
Firstly I thought it's because I didn't transpose the matrix, I tried to transpose it, it helped a little but didn't fix all bugs. Also, I tried to collect all transformations and apply them at once, but it was a very bad solution and didn't work well.
I need to have a separate rotation, scaling and transition, but rotation and transition are didn't work well because they are using the same segments of matrix. Also, I can't achieve normal rotation against Z-axis.
Here I compute 3D transformation matrices (4 x 4 matrices) for 2D translation, scale, and rotation. I also compose them with explicit concatenation in the style transform, or by pre multiplying the matrices.
const resultEl = document.getElementById('result')
const angleEl = document.getElementById('angle')
const xEl = document.getElementById('x')
const yEl = document.getElementById('y')
const sxEl = document.getElementById('sx')
const syEl = document.getElementById('sy')
const premulEl = document.getElementById('premul')
/** Counter clock-wise rotation */
function getXRotation(angle){
const c = Math.cos(angle)
const s = Math.sin(angle)
return [
+c, -s, 0, 0,
+s, +c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1];
}
function getTranslation(x, y){
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, 0, 1]
}
function getScale(sx, sy){
return [
sx, 0, 0, 0,
0,sy, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1]
}
function mat4mul(B, A){
const C = [];
for(let i = 0; i < 4; ++i){
for(let j = 0; j < 4; ++j){
let c = 0;
for(let k = 0; k < 4; ++k){
c += A[4*i+k] * B[4*k+j]
}
C.push(c);
}
}
return C
}
const update = (e) => {
const T1 = getTranslation(xEl.value, yEl.value)
const T2 = getXRotation(angleEl.value * Math.PI / 180)
const T3 = getScale(sxEl.value, syEl.value)
let T = '';
if(premul.checked){
T = `matrix3d(${
mat4mul(mat4mul(T1,T2),T3)
})`
resultEl.style.backgroundColor = 'yellow';
}else{
T = `
matrix3d(${T1})
matrix3d(${T2})
matrix3d(${T3})
`
resultEl.style.backgroundColor = 'gray'
}
resultEl.style.transform = T;
}
angleEl.addEventListener('input', update)
xEl.addEventListener('input', update)
yEl.addEventListener('input', update)
sxEl.addEventListener('input', update)
syEl.addEventListener('input', update)
premulEl.addEventListener('input', update)
div#result {
background-color: gray;
border: 2px solid black;
color: black;
width: 3cm;
height: 2cm;
}
<label>Rotation angle <input type=range id=angle min=0 max=360 value=0>
</label><br>
<label>Translation X <input type=range id=x min=0 max=360 value=0></label><br>
<label>Translation Y <input type=range id=y min=0 max=360 value=0></label><br>
<label>Scale X <input type=range id=sx min=0.5 max=2 value=1 step=0.01></label><br>
<label>Scale Y <input type=range id=sy min=0.5 max=2 value=1 step=0.01></label><br>
<label><input type=checkbox id=premul> Multiply matrices
<br>
<br>
<br>
<br>
<div id=result>Result</div>