I have a DIV Container and an image inside of it. The image has CSS transformations like: translate, rotate, scale
The image can be dragged by a user (and scaled, rotated) in the parent div. Sometimes, parts of the image can be outside of the parent div.
I want to create a new image, identical in size with the original image (with no rotation applied, with no scaling applied), but the part of the image (that is rotated/scaled) from the div that is not visible, should be fully white in the final image.
Let me show what I want to obtain using an image:
Original image used:
I tried various methods, but I fail to get the result I am looking for. Here is the code I came up with (with the help of ChatGPT also)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dynamic Image Transformation and Cropping</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.container {
width: 300px; /* You can change this to any size */
height: 300px; /* You can change this to any size */
border: 2px solid #000;
position: relative;
overflow: hidden;
margin-bottom: 20px;
}
.container img {
width: 150px; /* You can change this to any size */
height: 150px; /* You can change this to any size */
/* Apply CSS transformations */
transform: translate(-30px, -30px) rotate(45deg) scale(1.2);
transform-origin: center center;
position: absolute;
top: 0;
left: 0;
}
#buttons {
margin-bottom: 20px;
}
#tempCanvases {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
#tempCanvases canvas {
border: 1px solid #ccc;
}
</style>
</head>
<body>
<h1>Dynamic Image Transformation and Cropping</h1>
<div class="container">
<img id="sourceImage" src="https://i.sstatic.net/M6v4Hvlp.jpg" alt="Source Image">
</div>
<div id="buttons">
<button id="processButton">Process</button>
<button id="downloadButton" disabled>Download</button>
</div>
<h2>Temporary Canvases (For Debugging)</h2>
<div id="tempCanvases"></div>
<script>
// Wait for the DOM to load
document.addEventListener('DOMContentLoaded', () => {
const processButton = document.getElementById('processButton');
const downloadButton = document.getElementById('downloadButton');
const tempCanvasesDiv = document.getElementById('tempCanvases');
const sourceImage = document.getElementById('sourceImage');
let finalCanvas = null; // To store the final processed canvas
processButton.addEventListener('click', () => {
// Clear previous temporary canvases
tempCanvasesDiv.innerHTML = '';
finalCanvas = null;
downloadButton.disabled = true;
// Step 1: Get container and image dimensions
const container = sourceImage.parentElement;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const imageNaturalWidth = sourceImage.naturalWidth;
const imageNaturalHeight = sourceImage.naturalHeight;
const imageRenderedWidth = sourceImage.width;
const imageRenderedHeight = sourceImage.height;
console.log('Container Dimensions:', containerWidth, containerHeight);
console.log('Image Natural Dimensions:', imageNaturalWidth, imageNaturalHeight);
console.log('Image Rendered Dimensions:', imageRenderedWidth, imageRenderedHeight);
// Step 2: Get computed styles of the image
const style = window.getComputedStyle(sourceImage);
const transform = style.transform;
// If no transform is applied, set it to identity matrix
const matrix = transform === 'none' ? new DOMMatrix() : new DOMMatrix(transform);
// Extract transformation components
const scaleX = matrix.a;
const scaleY = matrix.d;
const rotateRadians = Math.atan2(matrix.b, matrix.a);
const rotateDegrees = rotateRadians * (180 / Math.PI);
const translateX = matrix.e;
const translateY = matrix.f;
console.log('Extracted Transformations:');
console.log('ScaleX:', scaleX);
console.log('ScaleY:', scaleY);
console.log('Rotate (degrees):', rotateDegrees);
console.log('TranslateX:', translateX);
console.log('TranslateY:', translateY);
// Step 3: Create the first temporary canvas (container size) with transformations applied
const tempCanvas1 = document.createElement('canvas');
tempCanvas1.width = containerWidth;
tempCanvas1.height = containerHeight;
const ctx1 = tempCanvas1.getContext('2d');
// Fill with white
ctx1.fillStyle = '#FFFFFF';
ctx1.fillRect(0, 0, tempCanvas1.width, tempCanvas1.height);
// Calculate the center of the image
const centerX = imageRenderedWidth / 2;
const centerY = imageRenderedHeight / 2;
// Apply transformations: translate, rotate, scale around the center
ctx1.translate(translateX + centerX, translateY + centerY); // Move to the center
ctx1.rotate(rotateRadians); // Apply rotation
ctx1.scale(scaleX, scaleY); // Apply scaling
ctx1.translate(-centerX, -centerY); // Move back
// Draw the image
ctx1.drawImage(sourceImage, 0, 0, imageRenderedWidth, imageRenderedHeight);
// Append the first temporary canvas for debugging
appendCanvas(tempCanvas1, 'Transformed Image');
// Step 4: Create the second temporary canvas to revert transformations and crop
const tempCanvas2 = document.createElement('canvas');
tempCanvas2.width = containerWidth;
tempCanvas2.height = containerHeight;
const ctx2 = tempCanvas2.getContext('2d');
// Fill with white
ctx2.fillStyle = '#FFFFFF';
ctx2.fillRect(0, 0, tempCanvas2.width, tempCanvas2.height);
// To revert transformations, apply inverse transformations
// Inverse scaling
const invScaleX = 1 / scaleX;
const invScaleY = 1 / scaleY;
// Inverse rotation
const invRotateRadians = -rotateRadians;
ctx2.translate(-translateX - centerX, -translateY - centerY); // Reverse translation
ctx2.translate(centerX, centerY); // Move to center
ctx2.rotate(invRotateRadians); // Apply inverse rotation
ctx2.scale(invScaleX, invScaleY); // Apply inverse scaling
ctx2.translate(-centerX, -centerY); // Move back
// Draw the image
ctx2.drawImage(sourceImage, 0, 0, imageRenderedWidth, imageRenderedHeight);
// Append the second temporary canvas for debugging
appendCanvas(tempCanvas2, 'Reverted Transformations');
// Step 5: Crop the image back to original size (natural image size)
// Create final canvas based on the image's natural size
finalCanvas = document.createElement('canvas');
finalCanvas.width = imageNaturalWidth;
finalCanvas.height = imageNaturalHeight;
const ctxFinal = finalCanvas.getContext('2d');
// Fill with white
ctxFinal.fillStyle = '#FFFFFF';
ctxFinal.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
// Calculate the scaling factor between rendered and natural size
const scaleFactorX = imageNaturalWidth / imageRenderedWidth;
const scaleFactorY = imageNaturalHeight / imageRenderedHeight;
// Draw the reverted image onto the final canvas
ctxFinal.drawImage(
tempCanvas2,
0, 0, containerWidth, containerHeight, // Source rectangle
0, 0, finalCanvas.width, finalCanvas.height // Destination rectangle
);
// Append the final canvas for debugging
appendCanvas(finalCanvas, 'Final Cropped Image');
// Enable the download button
downloadButton.disabled = false;
});
downloadButton.addEventListener('click', () => {
if (!finalCanvas) return;
// Convert the final canvas to a data URL
const dataURL = finalCanvas.toDataURL('image/png');
// Create a temporary link to trigger download
const link = document.createElement('a');
link.href = dataURL;
link.download = 'processed_image.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
/**
* Utility function to append a canvas with a label for debugging
* @param {HTMLCanvasElement} canvas
* @param {string} label
*/
function appendCanvas(canvas, label) {
const wrapper = document.createElement('div');
const caption = document.createElement('p');
caption.textContent = label;
wrapper.appendChild(caption);
wrapper.appendChild(canvas);
tempCanvasesDiv.appendChild(wrapper);
}
});
</script>
</body>
</html>
The 2D context setTransform
method accepts DOMMatrix
objects as arguments. Working entirely with such objects makes things easier, since once you've got it, you just need to apply its inverse()
to come back to the initial transform.
The tricky part might be to apply the transform-origin
on this DOMMatrix
object. There are several paths to get there, but basically it implies multiplying a first DOMMatrix
that translates to the origin, then the actual matrix and finally a last one to reset the origin to the new top-left.
Then you can use compositing to use a single canvas to render every steps.
// Wait for the DOM to load
document.addEventListener('DOMContentLoaded', () => {
const processButton = document.getElementById('processButton');
const downloadButton = document.getElementById('downloadButton');
const tempCanvasesDiv = document.getElementById('tempCanvases');
const sourceImage = document.getElementById('sourceImage');
let finalCanvas = null;
processButton.addEventListener('click', () => {
// Clear previous temporary canvases
tempCanvasesDiv.innerHTML = '';
downloadButton.disabled = true;
// Step 1: Get container and image dimensions
const container = sourceImage.parentElement;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const imageRenderedWidth = sourceImage.width;
const imageRenderedHeight = sourceImage.height;
// Step 2: Get computed styles of the image
const style = window.getComputedStyle(sourceImage);
const transform = style.transform;
// Calculate the center of the image
const centerX = imageRenderedWidth / 2;
const centerY = imageRenderedHeight / 2;
// If no transform is applied, set it to identity matrix
const matrix = transform === 'none' ?
new DOMMatrix() :
new DOMMatrix(transform)
// Apply the transform-origin
.preMultiplySelf({ e: centerX, f: centerY })
.multiply({ e: -centerX, f: -centerY });
finalCanvas = document.createElement('canvas');
finalCanvas.width = containerWidth;
finalCanvas.height = containerHeight;
const ctx1 = finalCanvas.getContext('2d');
ctx1.setTransform(matrix);
// Draw the image transformed
ctx1.drawImage(sourceImage, 0, 0, imageRenderedWidth, imageRenderedHeight);
// Draw again, using the inverse transform
ctx1.setTransform(matrix.inverse());
// Replace the previous content by the new content
ctx1.globalCompositeOperation = "copy";
ctx1.drawImage(ctx1.canvas, 0, 0);
// Fill with white below the current drawing
ctx1.fillStyle = '#FFFFFF';
ctx1.globalCompositeOperation = "destination-over"; // Draw below
ctx1.resetTransform(); // No transform
ctx1.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
// Append the canvas for debugging
appendCanvas(finalCanvas, 'Result');
// Enable the download button
downloadButton.disabled = false;
});
downloadButton.addEventListener('click', () => {
if (!finalCanvas) return;
// Convert the final canvas to a data URL
const dataURL = finalCanvas.toDataURL('image/png');
// Create a temporary link to trigger download
const link = document.createElement('a');
link.href = dataURL;
link.download = 'processed_image.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
/**
* Utility function to append a canvas with a label for debugging
* @param {HTMLCanvasElement} canvas
* @param {string} label
*/
function appendCanvas(canvas, label) {
const wrapper = document.createElement('div');
const caption = document.createElement('p');
caption.textContent = label;
wrapper.appendChild(caption);
wrapper.appendChild(canvas);
tempCanvasesDiv.appendChild(wrapper);
}
});
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.container {
width: 300px;
/* You can change this to any size */
height: 300px;
/* You can change this to any size */
border: 2px solid #000;
position: relative;
overflow: hidden;
margin-bottom: 20px;
}
.container img {
width: 150px;
/* You can change this to any size */
height: 150px;
/* You can change this to any size */
/* Apply CSS transformations */
transform: translate(-30px, -30px) rotate(45deg) scale(1.2);
transform-origin: center center;
position: absolute;
top: 0;
left: 0;
}
#buttons {
margin-bottom: 20px;
}
#tempCanvases {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
#tempCanvases canvas {
border: 1px solid #ccc;
}
<h1>Dynamic Image Transformation and Cropping</h1>
<div class="container">
<img id="sourceImage" src="https://i.sstatic.net/M6v4Hvlp.jpg" crossorigin alt="Source Image">
</div>
<div id="buttons">
<button id="processButton">Process</button>
<button id="downloadButton" disabled>Download</button>
</div>
<h2>Temporary Canvases (For Debugging)</h2>
<div id="tempCanvases"></div>