I’m working on a web application which uses fabric.js to make drawings on the web page’s canvas.
I will tell you about some general properties of this tool for context:
- Canvas is a space that has its own cartesian coordinate system with reverted $x-axis$ and $y-axis$ meaning the valeus of
xandyincrease as we go towards right and bottom. - Each object created in canvas such as rectangle, text etc. has their own
3x3transformation matrix that tells the position/translation of the object’s center. This transformation matrix tells us where the object is relative to the center of the canvas space. - We can use each object’s transformation matrix to calculate where other objects are located relative to that object. To calculate where object
Bis located relative to objectA, we can do the below calculation:
T_A * X = T_B
(T_A)' * T_A * X = (T_A)' * T_B
X = (T_A)' * T_B
Here, X is the unknown transform matrix that tells where the object B is at relative to object A, it also defines a relationship between these objects. T_A is object A‘s transformation matrix and T_B is object B‘s transformation matrix. Prime notation is used to notate that it’s inverse matrix.
Now, I will scale the object A in x-axis by 2, doubling its width. This will cause T_A to change, we can calculate object B‘s new transform matrix using the relationship transform matrix we found in the previous step.
T_{B}=T_{A}*X
When I apply the newly found T_B transformation matrix to the object B it successfully shifts the center of the object B twice as before since we have scaled object A in x-axis by factor of 2. Object B‘s previous translation in x-axis was -61.98 and it’s now -123.97 (roughly doubled). In this process, the relationship matrix X was unchanged and used to calculate new transform matrix of object B. Everything is good so far!
Now, what I’m having trouble is avoiding object B from being also scaled or reverting the scaling applied to object it. As you can see, in the first picture text was smaller but when the object A got scaled by 2 in second picture, it also got scaled. This causes text to be drawn disrupted. I want the text to look as same as it looked in first picture but preserving its relative position to the parent object which is object A. It should remain right below the bottom left corner of rectangle. The scale applied to transformation matrix of object B seem to be also causing its translation properties in x and y-axes to be scaled in some way. Because when I revert the scaling of object B programmatically by setting the text’s scale property back to its original value which is 1, text appears slightly off in the direction of the scale applied. Below is the picture where I revert the scaling applied to object B programmatically:
I need a way to prevent object B from scaling somehow multiplying the T_B we have found with some kind of transform matrix that will revert the effect of scaling applied on it’s translation values in x and y-axes. But I couldn’t figure out what kind of transform matrix I should be using.
NOTE: When we rotate the object, rotation matrix also gets into action in the calculations. Please think throughly in a case where we also rotated the parent object A.
Working demo
const outputEl = document.getElementById('output-table-body');
const rowTemplate = document.getElementById('output-table-row');
const matricesEl = document.getElementById('output-matrices');
const canvasEl = document.getElementById('playground');
const canvas = new fabric.Canvas(canvasEl);
const formatNumber = (number) => {
return parseFloat(number).toFixed(2);
};
const mouseDownHandler = (opt) => {
let evt = opt.e;
if (evt.altKey === true) {
canvas.isDragging = true;
canvas.selection = false;
canvas.lastPosX = evt.clientX;
canvas.lastPosY = evt.clientY;
}
};
const mouseMoveHandler = (opt) => {
if (canvas.isDragging) {
let e = opt.e;
let vpt = canvas.viewportTransform;
vpt[4] += e.clientX - canvas.lastPosX;
vpt[5] += e.clientY - canvas.lastPosY;
canvas.requestRenderAll();
canvas.lastPosX = e.clientX;
canvas.lastPosY = e.clientY;
}
};
const mouseUpHandler = (opt) => {
// on mouse up we want to recalculate new interaction
// for all objects, so we call setViewportTransform
canvas.setViewportTransform(canvas.viewportTransform);
canvas.isDragging = false;
canvas.selection = true;
};
const zoomHandler = (opt) => {
let delta = opt.e.deltaY;
let zoom = canvas.getZoom();
zoom *= 0.999 ** delta;
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
};
const outputObjects = () => {
outputEl.innerHTML = '';
matricesEl.innerHTML = '';
[parent, child].forEach((object) => {
const transformMatrix = object.calcTransformMatrix();
const opt = fabric.util.qrDecompose(transformMatrix);
const rowEl = rowTemplate.content.cloneNode(true);
const tdEls = rowEl.querySelectorAll('td');
tdEls[0].innerText = object.name;
tdEls[1].innerText = formatNumber(opt.scaleX);
tdEls[2].innerText = formatNumber(opt.scaleY);
tdEls[3].innerText = formatNumber(opt.angle);
tdEls[4].innerText = formatNumber(opt.translateX);
tdEls[5].innerText = formatNumber(opt.translateY);
outputEl.appendChild(rowEl);
});
const pM = parent.calcTransformMatrix();
const cM = child.calcTransformMatrix();
const rM = child.relationship;
matricesEl.innerHTML += `
<p>Parent transform matrix</p>
<p>=</p>
<table>
<tbody>
<tr>
<td>${formatNumber(pM[0])}</td>
<td>${formatNumber(pM[2])}</td>
<td>${formatNumber(pM[4])}</td>
</tr>
<tr>
<td>${formatNumber(pM[1])}</td>
<td>${formatNumber(pM[3])}</td>
<td>${formatNumber(pM[5])}</td>
</tr>
<tr>
<td>0</td>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
`;
matricesEl.innerHTML += `
<p>Children transform matrix</p>
<p>=</p>
<table>
<tbody>
<tr>
<td>${formatNumber(cM[0])}</td>
<td>${formatNumber(cM[2])}</td>
<td>${formatNumber(cM[4])}</td>
</tr>
<tr>
<td>${formatNumber(cM[1])}</td>
<td>${formatNumber(cM[3])}</td>
<td>${formatNumber(cM[5])}</td>
</tr>
<tr>
<td>0</td>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
`;
matricesEl.innerHTML += `
<p>Relationship transform matrix</p>
<p>=</p>
<table>
<tbody>
<tr>
<td>${formatNumber(rM[0])}</td>
<td>${formatNumber(rM[2])}</td>
<td>${formatNumber(rM[4])}</td>
</tr>
<tr>
<td>${formatNumber(rM[1])}</td>
<td>${formatNumber(rM[3])}</td>
<td>${formatNumber(rM[5])}</td>
</tr>
<tr>
<td>0</td>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
`;
};
const selectionCreatedOrUpdatedHandler = (opt) => {
outputObjects();
};
const selectionClearedHandler = () => {
outputObjects();
};
const objectUpdateHandler = () => {
outputObjects();
};
canvas.on('mouse:wheel', zoomHandler);
canvas.on('mouse:down', mouseDownHandler);
canvas.on('mouse:move', mouseMoveHandler);
canvas.on('mouse:up', mouseUpHandler);
canvas.on('object:moving', objectUpdateHandler);
canvas.on('object:scaling', objectUpdateHandler);
canvas.on('object:rotating', objectUpdateHandler);
canvas.on('selection:created', selectionCreatedOrUpdatedHandler);
canvas.on('selection:updated', selectionCreatedOrUpdatedHandler);
canvas.on('selection:cleared', selectionClearedHandler);
const canvasCenterPoint = new fabric.Circle({
radius: 5,
fill: 'red',
top: -2.5,
left: -2.5,
selectable: false,
evented: false
});
const parent = new fabric.Rect({
name: 'Parent',
top: -100,
left: -100,
width: 199,
height: 199,
fill: 'transparent',
stroke: 'red',
strokeWidth: 1,
});
const child = new fabric.IText('Child text', {
name: 'Child',
top: 101,
left: -100,
fontSize: 18,
fontFamily: 'Arial',
});
const parentM = parent.calcTransformMatrix();
const parentInvM = fabric.util.invertTransform(parentM);
child.relationship = fabric.util.multiplyTransformMatrices(
parentInvM,
child.calcTransformMatrix()
);
const update = () => {
const parentMatrix = parent.calcTransformMatrix();
const newM = fabric.util.multiplyTransformMatrices(
parentMatrix,
child.relationship
);
const opt = fabric.util.qrDecompose(newM);
child.set({
flipX: false,
flipY: false,
});
child.setPositionByOrigin(
{
x: opt.translateX,
y: opt.translateY
},
'center',
'center'
);
child.set({
...opt,
scaleX: 1,
scaleY: 1,
});
child.setCoords();
canvas.requestRenderAll();
};
parent.on('moving', update);
parent.on('rotating', update);
parent.on('scaling', update);
canvas.add(parent, child, canvasCenterPoint);
canvas.viewportTransform[4] = 400;
canvas.viewportTransform[5] = 250;
const center = () => {
parent.setPositionByOrigin({
x: 0,
y: 0,
}, 'center', 'center');
parent.setCoords();
update();
}
outputObjects();
html {
font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
color: #cccccc;
}
body {
margin: 50px 0 0 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #222222;
}
#playground {
border: 1px solid #666666;
}
.output {
display: flex;
flex-direction: column;
row-gap: 40px;
}
.output__heading {
font-size: 2rem;
}
.output__matrices {
display: grid;
grid-template-rows: auto;
grid-template-columns: auto auto 1fr;
grid-column-gap: 5px;
grid-row-gap: 10px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fabric Test</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<canvas id="playground" width="800" height="500"></canvas>
<div class="output">
<div class="output__transform-matrices" id="outputs">
<table>
<thead>
<tr>
<th>Name</th>
<th>ScaleX</th>
<th>ScaleY</th>
<th>Angle</th>
<th>TranslateX</th>
<th>TranslateY</th>
</tr>
</thead>
<tbody id="output-table-body"></tbody>
</table>
<div class="output__matrices" id="output-matrices"></div>
</div>
</div>
<template id="output-table-row">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</template>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/fabric.min.js"
></script>
</body>
</html>


