109
LVL 04 — SENIOR-IN-TRAININGSESSION 109DAY 109

SELECTION & TRANSFORM

🎫 PIXELCRAFT-095
Feature | 🔴 Expert | Priority: 🟠 High

Users can apply filters to the whole image but can't select and transform regions. Implement selection tools with bounding boxes, resize handles, rotation, and flip. Real image editor capabilities with real math.
CONCEPTS.UNLOCKED
🔲
Bounding Boxes
A rectangle that encloses a selection. Defined by { x, y, width, height }. The simplest selection primitive. Click and drag to create. Handles at corners and edges for resizing.
📐
Affine Transformations
Translate, rotate, scale, skew. All expressible as matrix multiplication. A 3×3 matrix can encode any combination of these transforms. The GPU uses this math for every pixel on screen.
🎯
Hit Testing
Is the mouse cursor over a handle, inside the box, or outside? Check distance from cursor to each handle, check if point is inside rectangle. Different cursor styles for different zones.
🔄
Rotation
Rotate selection around its center point. atan2(dy, dx) gives the angle from center to mouse. Canvas ctx.rotate() applies the rotation. Handles rotate with the selection.
↔️
Transform Handles
8 resize handles + 1 rotation handle. Corners resize both dimensions. Edges resize one dimension. Top handle rotates. Each handle changes the cursor to indicate its function.
🧮
Matrix Math
Canvas 2D transforms use a 3×3 matrix. ctx.setTransform(a, b, c, d, e, f) — where a,d scale, b,c skew, e,f translate. Composing transforms = multiplying matrices. Order matters.
HANDS-ON.TASKS
01
Selection State
// 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 };
02
Draw Selection Box & Handles
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(); }
03
Hit Testing
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); }
04
Rotation with atan2
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.
05
Flip & Apply Transform
// 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); }
06
Close the Ticket
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 ✅
CS.DEEP-DIVE

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.

// The 2D transform matrix:

| a  c  e |   a,d = scale
| b  d  f |   b,c = skew
| 0  0  1 |   e,f = translate

Identity (no transform):
| 1  0  0 |
| 0  1  0 |
| 0  0  1 |

Scale 2x:
| 2  0  0 |
| 0  2  0 |
| 0  0  1 |

Rotate 45°:
| cos(45°)  -sin(45°)  0 |
| sin(45°)   cos(45°)  0 |
| 0          0         1 |

// Composing: multiply matrices.
// GPU does this billions of times/sec.
// Same math: 2D canvas, CSS, WebGL,
// game engines, robotics, CAD.
"Transform Lab"
[A]Add lasso selection: freehand drawing to select irregular regions. Use Canvas path methods (beginPath, lineTo, closePath) and isPointInPath() for hit testing. Much harder than rectangles — real Photoshop territory.
[B]Add aspect-ratio lock: hold Shift while resizing to maintain the original proportions. Calculate the constrained width/height based on which handle is being dragged.
[C]Research: how does CSS transform work under the hood? transform: rotate(45deg) scale(2) is matrix multiplication. Inspect with getComputedStyle → see the matrix() values. Same math, different API.
REF.MATERIAL
ARTICLE
MDN Web Docs
Official Canvas transform reference: translate, rotate, scale, setTransform, getTransform. The matrix behind every canvas operation.
CANVASOFFICIALESSENTIAL
VIDEO
3Blue1Brown
The most beautiful math visualization: what matrices really mean geometrically. Transforms as linear maps. Understanding transforms visually before algebraically.
MATHVISUALESSENTIAL
ARTICLE
MDN Web Docs
The most useful trig function in graphics: returns the angle between the positive x-axis and the point (x, y). Essential for rotation, aiming, and angle calculations.
MATHOFFICIAL
ARTICLE
HTML5 Canvas Tutorials
Practical guide to canvas transforms: save/restore state, composing transforms, coordinate system changes. Hands-on examples.
CANVASTUTORIAL
VIDEO
The Coding Train
Interactive selection tool implementation: drag to select, resize handles, bounding boxes. Step-by-step canvas programming.
CANVASSELECTION
// LEAVE EXCITED BECAUSE
Select a region, drag to move, pull handles to resize, grab the top handle to rotate, Ctrl+H to flip. Real image editor capabilities powered by real math. This is Photoshop-level functionality you built from scratch.