function drawCropOverlay(
ctx, crop, canvasW, canvasH
) {
// Darken area outside crop
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(0, 0, canvasW, canvasH);
// Clear the crop region (show image)
ctx.clearRect(
crop.x, crop.y,
crop.width, crop.height
);
// Draw border
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.strokeRect(
crop.x, crop.y,
crop.width, crop.height
);
// Draw rule-of-thirds grid
drawThirdsGrid(ctx, crop);
// Draw resize handles
const handles = getHandlePositions(crop);
handles.forEach(h => {
ctx.fillStyle = '#fff';
ctx.fillRect(
h.x - 4, h.y - 4, 8, 8);
});
}
function getHandlePositions(crop) {
const { x, y, width: w, height: h }
= crop;
return [
{ x, y, cursor: 'nw-resize' },
{ x: x+w/2, y, cursor: 'n-resize' },
{ x: x+w, y, cursor: 'ne-resize' },
{ x: x+w, y: y+h/2,
cursor: 'e-resize' },
{ x: x+w, y: y+h,
cursor: 'se-resize' },
{ x: x+w/2, y: y+h,
cursor: 's-resize' },
{ x, y: y+h, cursor: 'sw-resize' },
{ x, y: y+h/2, cursor: 'w-resize' },
];
}
const CROP_PRESETS = {
free: null,
square: 1 / 1,
portrait: 4 / 5, // Instagram
landscape: 16 / 9, // YouTube
wide: 1.91 / 1, // Facebook
story: 9 / 16, // Stories/Reels
twitter: 3 / 1, // Twitter header
};
function constrainToRatio(
crop, ratio, handle
) {
if (!ratio) return crop; // Free crop
// Adjust height based on width
const newCrop = { ...crop };
if (handle.includes('e')
|| handle.includes('w')) {
newCrop.height =
newCrop.width / ratio;
} else {
newCrop.width =
newCrop.height * ratio;
}
return newCrop;
}
function applyCrop(
sourceCanvas, crop
) {
const output =
document.createElement('canvas');
output.width = crop.width;
output.height = crop.height;
const ctx = output.getContext('2d');
// Draw only the cropped region
ctx.drawImage(
sourceCanvas,
crop.x, crop.y, // source x,y
crop.width, // source w
crop.height, // source h
0, 0, // dest x,y
crop.width, // dest w
crop.height // dest h
);
return output;
}
function rotateCanvas(
sourceCanvas, degrees
) {
const radians =
degrees * Math.PI / 180;
// Calculate new canvas size
// to fit rotated image
const sin = Math.abs(Math.sin(radians));
const cos = Math.abs(Math.cos(radians));
const w = sourceCanvas.width;
const h = sourceCanvas.height;
const newW = w * cos + h * sin;
const newH = w * sin + h * cos;
const output =
document.createElement('canvas');
output.width = newW;
output.height = newH;
const ctx = output.getContext('2d');
// Translate to center, rotate, draw
ctx.translate(newW / 2, newH / 2);
ctx.rotate(radians);
ctx.drawImage(
sourceCanvas, -w / 2, -h / 2);
return output;
}
git switch -c feature/PIXELCRAFT-064-crop-rotate
git add src/
git commit -m "Add crop with presets + free rotate (PIXELCRAFT-064)"
git push origin feature/PIXELCRAFT-064-crop-rotate
# PR → Review → Merge → Close ticket ✅
Transformations are linear algebra in action.
Every translate, rotate, and scale is a matrix multiplication. The GPU does millions of these per frame.