// types/selection.ts
interface Selection {
x: number;
y: number;
width: number;
height: number;
rotation: number; // radians
}
interface Handle {
position: 'nw' | 'n' | 'ne' | 'e'
| 'se' | 's' | 'sw' | 'w'
| 'rotate';
cursor: string;
x: number;
y: number;
}
type DragMode =
| { type: 'none' }
| { type: 'creating';
startX: number;
startY: number }
| { type: 'moving';
offsetX: number;
offsetY: number }
| { type: 'resizing';
handle: Handle['position'] }
| { type: 'rotating';
startAngle: number };
function drawSelection(
ctx: CanvasRenderingContext2D,
sel: Selection
) {
ctx.save();
// Transform to selection center
const cx = sel.x + sel.width / 2;
const cy = sel.y + sel.height / 2;
ctx.translate(cx, cy);
ctx.rotate(sel.rotation);
ctx.translate(-cx, -cy);
// Dashed border
ctx.strokeStyle = '#00bcd4';
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
ctx.strokeRect(
sel.x, sel.y,
sel.width, sel.height);
// Resize handles (8 squares)
ctx.setLineDash([]);
const handleSize = 8;
const handles = getHandlePositions(
sel, handleSize);
handles.forEach(h => {
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#00bcd4';
ctx.fillRect(
h.x - handleSize / 2,
h.y - handleSize / 2,
handleSize, handleSize);
ctx.strokeRect(
h.x - handleSize / 2,
h.y - handleSize / 2,
handleSize, handleSize);
});
// Rotation handle (circle above)
const rotHandle =
{ x: cx, y: sel.y - 25 };
ctx.beginPath();
ctx.arc(
rotHandle.x, rotHandle.y,
6, 0, Math.PI * 2);
ctx.fillStyle = '#00bcd4';
ctx.fill();
ctx.restore();
}
function hitTest(
mx: number, my: number,
sel: Selection
): DragMode {
const handleSize = 10;
const handles =
getHandlePositions(sel, handleSize);
// Check rotation handle first
const rotHandle =
{ x: sel.x + sel.width / 2,
y: sel.y - 25 };
if (distance(mx, my,
rotHandle.x, rotHandle.y)
< handleSize) {
return {
type: 'rotating',
startAngle: Math.atan2(
my - (sel.y + sel.height / 2),
mx - (sel.x + sel.width / 2)),
};
}
// Check resize handles
for (const h of handles) {
if (distance(mx, my, h.x, h.y)
< handleSize) {
return {
type: 'resizing',
handle: h.position,
};
}
}
// Check inside selection
if (mx >= sel.x
&& mx <= sel.x + sel.width
&& my >= sel.y
&& my <= sel.y + sel.height) {
return {
type: 'moving',
offsetX: mx - sel.x,
offsetY: my - sel.y,
};
}
return { type: 'none' };
}
function distance(
x1: number, y1: number,
x2: number, y2: number
) {
return Math.sqrt(
(x2 - x1) ** 2 + (y2 - y1) ** 2);
}
function handleRotation(
mx: number, my: number,
sel: Selection,
startAngle: number
): number {
const cx = sel.x + sel.width / 2;
const cy = sel.y + sel.height / 2;
// atan2 gives angle from center
// to mouse position
const currentAngle = Math.atan2(
my - cy, mx - cx);
const delta =
currentAngle - startAngle;
// Snap to 15° increments if
// Shift key held
if (shiftHeld) {
const snap = Math.PI / 12; // 15°
return Math.round(
(sel.rotation + delta) / snap)
* snap;
}
return sel.rotation + delta;
}
// atan2(y, x) → angle in radians
// Returns -π to π
// (0,1)=0° (1,0)=90° (0,-1)=180°
// The most useful trig function
// in graphics programming.
// Flip selection horizontally
function flipHorizontal(
ctx: CanvasRenderingContext2D,
sel: Selection
) {
const cx = sel.x + sel.width / 2;
const cy = sel.y + sel.height / 2;
// Extract selection pixels
const imageData = ctx.getImageData(
sel.x, sel.y,
sel.width, sel.height);
ctx.save();
ctx.translate(cx, cy);
ctx.scale(-1, 1); // flip X axis
ctx.translate(-cx, -cy);
ctx.putImageData(
imageData, sel.x, sel.y);
ctx.restore();
}
// Apply final transform and clear
// selection
function applyTransform(
sel: Selection
) {
// Render transformed pixels to
// a new canvas, then composite
// back into the main canvas.
// Selection complete.
pushHistory(currentImageData);
}
git switch -c feature/PIXELCRAFT-095-selection-transform
git add src/tools/ src/components/
git commit -m "Add selection + transform tools (PIXELCRAFT-095)"
git push origin feature/PIXELCRAFT-095-selection-transform
# PR → Review → Merge → Close ticket ✅
Affine transformations: the math behind every pixel on screen.
Every image, every UI element, every 3D object is positioned using matrix multiplication. A single 3×3 matrix encodes translate + rotate + scale + skew.