I have the following HTML structure:
<body>
<button onclick={appendBlueToGreen()}>Append To Green</button>
<div id="red" style="transform: rotate(20deg); width: 100px; height: 100px; position: absolute; top: 50px; left: 50px; background-color: red;">
<div id="green" style="transform: rotate(20deg); width: 80px; height: 80px; position: absolute; top: 13px; left: 11px; background-color: green;">
</div>
</div>
<div id="blue" style="transform: rotate(45deg); width: 50px; height: 50px; position: absolute; top: 29px; left: 313px; background-color: blue;"></div>
</body>
I want to append the #blue
element to the #green
element, but the #blue
element should maintain its exact position on the screen. In other words, the #blue
element should be a child of the #green
element, but its visual position and rotation should remain unchanged. So whether the #blue
element is attached to #green
, <body>
, or any other element, it should remain in the same position as in the image below.
To achieve this, I need to:
#green
and #red
elements
relative to the document body.#blue
element based on the
calculated position and rotation of the #red
and #green
element, so that it
appears at the same position on the screen but within the #green
element.#blue
element to negate the effect of the
rotations applied to the #green
and #red
elements.I'm struggling to figure out the correct mathematical calculations to achieve this. I would greatly appreciate if someone could provide a JavaScript solution to achive this.
Here is the code that I have used with the help of AI to achieve this, but it was not successful: codepen
Insofar the rotation is involved, the formula is quite simple:
newBlueRotation = originalBlueRotation - redRotation - greenRotation;
However, for the new positions of the element, it is unexpectedly complicated. That is because each rotation is performed around a centre that is different from the reference point used for positioning. The centre is
by default the centre of the div, but it can be altered using
transform-origin
css property.
Thus, for finding the actual centre of rotation of an element
one may use:
window.getComputedStyle(element).transformOrigin
The difference between the centre of rotation and the reference point means that in order to compute the positions of elements included down the line, for each included element one has to translate from the reference point to the centre, apply the rotation and then translate back to the reference point.
For instance, the reference point of only one rotated rectangle, the red one is found by this operation (I hope this pseudo-code notation is self-explanatory):
redReferencePoint = redOffset + redCenter - redCenter * rotate(-red)
Offsets are positions of the elements with respect to their parents (all assumed absolutely positioned for the calculation to work).
Apply these calculations sequentially for the red->green->new blue elements, setting the condition that the position of the new blue element is the same as the position of the original blue one, one gets an equation that can be solved for the new blue offset, which is huge:
newBlueOffset = greenCenter - blueCenter +
( blueOffset + blueCenter - redOffset - redCenter -
(greenOffset - redCenter + greenCenter) * rotate(-red)
) * rotate(red+green)
The complicated computations have also the effect of the result being slightly imprecise, probably due to round-off errors - there's sometimes a minute "subpixel" repositioning of the blue element,
Maybe the formulae can be better seen in code; some of the code in this snippet comes from the codepen in the question.
const appendBlueToGreen = () => {
// Get the elements
const blueElement = document.getElementById('blue');
const greenElement = document.getElementById('green');
const redElement = document.getElementById('red');
// Function to convert degree to radian
const radToDeg = (deg) => (deg * 180) / Math.PI;
// get the rotation center
const getCenter = element => {
try{
const [x, y] = window.getComputedStyle(element).transformOrigin.split(' ').map(parseFloat);
return {x, y};
}
catch(err){
// default is element center
return {x: element.offsetWidth / 2, y: element.offsetHeight / 2};
}
}
// Function to get the rotation angle of an element (in radians)
const getRotation = (element) => {
const transform = window.getComputedStyle(element).transform;
const matrix = transform.match(/^matrix\((.+)\)$/);
if (matrix) {
const values = matrix[1].split(', ');
const a = values[0];
const b = values[1];
return Math.atan2(b, a);
} else {
return 0;
}
};
const getOffset = (element) => {
//return {x: parseFloat(window.getComputedStyle(element).left),
// y: parseFloat(window.getComputedStyle(element).top)};
return {x: element.offsetLeft, y: element.offsetTop};
}
const rotateVec = ({x, y}, theta) => { // rotate vector {x, y} by theta clockwise
const cosTheta = Math.cos(theta),
sinTheta = Math.sin(theta);
return {x: x * cosTheta + y * sinTheta, y: -x * sinTheta + y * cosTheta};
};
const addVec = ({x: x1, y: y1}, {x: x2, y: y2}, ...rest) => {
const sum12 = {x: x1 + x2, y: y1 + y2};
return rest.length === 0 ? sum12 : addVec(sum12, ...rest);
}
const negVec = ({x, y}) => ({x: -x, y: -y});
const roundToFixed = (x, digits) => x.toFixed(digits).replace(/\.?0+$/, '');
// Calculate the rotations
const redRotation = getRotation(redElement);
const greenRotation = getRotation(greenElement);
const blueRotation = getRotation(blueElement);
// calculate the offset positions of elements wrt their parents
const redOffset = getOffset(redElement);
const greenOffset = getOffset(greenElement);
const blueOffset = getOffset(blueElement);
// calculate the centers
const redCenter = getCenter(redElement);
const greenCenter = getCenter(greenElement);
const blueCenter = getCenter(blueElement);
let newOffset = addVec(negVec(greenOffset), negVec(greenCenter), redCenter);
newOffset = rotateVec(newOffset, -redRotation);
newOffset = addVec(newOffset, blueOffset, blueCenter, negVec(redOffset), negVec(redCenter));
newOffset = rotateVec(newOffset, greenRotation + redRotation);
newOffset = addVec(newOffset, greenCenter, negVec(blueCenter));
// Set the new position and rotation for the blue element
blueElement.style.position = 'absolute';
blueElement.style.left = `${roundToFixed(newOffset.x, 2)}px`;
blueElement.style.top = `${roundToFixed(newOffset.y, 2)}px`;
const newBlueRotation = roundToFixed(radToDeg(blueRotation - redRotation - greenRotation), 2);
blueElement.style.transform = `rotate(${newBlueRotation}deg)`;
// Append the blue element to the green element
greenElement.appendChild(blueElement);
console.log(blueElement.style.left, blueElement.style.top, blueElement.style.transform)
this.event.target.setAttribute("disabled","d");
};
window.appendBlueToGreen = appendBlueToGreen;
<button onclick={appendBlueToGreen()}>Append To Green</button>
<div id="red" style="transform: rotate(20deg);width: 100px;height: 100px;position: absolute;top: 50px;left: 50px;background-color: red;">
<div id="green" style="transform: rotate(20deg);width: 80px;height: 80px;position: absolute;top: 13px;left: 11px;background-color: green;">
</div>
</div>
<div id="blue" style="transform: rotate(45deg);width: 50px;height: 50px;position: absolute;top: 29px;left: 313px;background-color: blue;"></div>