Crop and straighten

크롭 앤 스트레이튼

Rotates a tilted horizon back level and trims away unneeded edges to recompose the frame — the most basic editing step there is.

Also known as: Crop & straighten수평 맞추기
···
html
<div class="wrap">
  <div class="stage"><canvas id="c" width="300" height="180"></canvas></div>
  <div class="hud"><span>회전 후 빈 모서리를 크롭으로 제거</span><b id="ang">0.0°</b></div>
</div>
css
.wrap{position:absolute;inset:0;padding:clamp(8px,3vmin,20px);display:flex;flex-direction:column;gap:6px}
.stage{position:relative;flex:1;min-height:0}
canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;border-radius:10px;border:1px solid var(--line);background:#0a0a0a}
.hud{display:flex;justify-content:space-between;gap:8px;font:700 clamp(9px,2.2vmin,11px)/1 ui-monospace,monospace;color:var(--muted)}
.hud b{color:var(--fg)}
js
var W = 300, H = 180;
var pc = document.createElement('canvas'); pc.width = W; pc.height = H;
var pctx = pc.getContext('2d');

function paintPhoto(cx, w, h) {
  var g = cx.createLinearGradient(0, 0, 0, h * 0.62);
  g.addColorStop(0, '#5b86c9'); g.addColorStop(0.55, '#bcd6ea'); g.addColorStop(1, '#f5e0ad');
  cx.fillStyle = g; cx.fillRect(0, 0, w, h);
  var sx = w * 0.74, sy = h * 0.22, sr = h * 0.11;
  var sg = cx.createRadialGradient(sx, sy, 0, sx, sy, sr * 3.2);
  sg.addColorStop(0, 'rgba(255,247,214,0.9)'); sg.addColorStop(1, 'rgba(255,247,214,0)');
  cx.fillStyle = sg; cx.fillRect(0, 0, w, h * 0.62);
  cx.fillStyle = '#fff8df'; cx.beginPath(); cx.arc(sx, sy, sr, 0, 7); cx.fill();
  cx.fillStyle = '#6f9152';
  cx.beginPath(); cx.moveTo(0, h * 0.64);
  for (var x = 0; x <= w; x += 14) cx.lineTo(x, h * 0.64 - Math.sin(x * 0.014) * h * 0.05 - Math.sin(x * 0.032 + 1.2) * h * 0.02);
  cx.lineTo(w, h); cx.lineTo(0, h); cx.closePath(); cx.fill();
  cx.fillStyle = '#405f38';
  cx.beginPath(); cx.moveTo(0, h * 0.79);
  for (var x2 = 0; x2 <= w; x2 += 14) cx.lineTo(x2, h * 0.79 - Math.sin(x2 * 0.022 + 0.6) * h * 0.035);
  cx.lineTo(w, h); cx.lineTo(0, h); cx.closePath(); cx.fill();
  var tx = w * 0.2, ty = h * 0.8;
  cx.strokeStyle = '#3a2a1c'; cx.lineWidth = Math.max(1.5, w * 0.008);
  cx.beginPath(); cx.moveTo(tx, ty); cx.lineTo(tx - w * 0.01, ty - h * 0.13); cx.stroke();
  cx.fillStyle = '#2e4a28';
  for (var i = 0; i < 5; i++) { cx.beginPath(); cx.arc(tx + (i - 2) * w * 0.013, ty - h * 0.15 - (i % 2) * h * 0.018, w * 0.028, 0, 7); cx.fill(); }
  cx.fillStyle = '#1e1e1c';
  cx.beginPath(); cx.arc(w * 0.56, h * 0.745, h * 0.018, 0, 7); cx.fill();
  cx.fillRect(w * 0.555, h * 0.76, w * 0.01, h * 0.06);
  var img = cx.getImageData(0, 0, w, h), d = img.data;
  for (var p = 0; p < d.length; p += 4) { var n = (Math.random() - 0.5) * 10; d[p] += n; d[p + 1] += n; d[p + 2] += n; }
  cx.putImageData(img, 0, 0);
}
paintPhoto(pctx, W, H);
var c = document.getElementById('c'), ctx = c.getContext('2d');
var t = 0;
function frame() {
  t += 0.012;
  var angleDeg = Math.sin(t) * 6;
  var angle = angleDeg * Math.PI / 180;
  ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0, 0, W, H);
  ctx.save();
  ctx.translate(W / 2, H / 2);
  ctx.rotate(angle);
  ctx.drawImage(pc, -W / 2, -H / 2);
  ctx.restore();
  var marginX = W * Math.abs(Math.sin(angle)) * 0.55;
  var marginY = H * Math.abs(Math.sin(angle)) * 0.55;
  var cx0 = marginX, cy0 = marginY, cw = W - marginX * 2, ch = H - marginY * 2;
  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.fillRect(0, 0, W, cy0);
  ctx.fillRect(0, cy0 + ch, W, H - (cy0 + ch));
  ctx.fillRect(0, cy0, cx0, ch);
  ctx.fillRect(cx0 + cw, cy0, W - (cx0 + cw), ch);
  ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.strokeRect(cx0, cy0, cw, ch);
  ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 1;
  for (var k = 1; k < 3; k++) {
    var gx = cx0 + cw * k / 3;
    ctx.beginPath(); ctx.moveTo(gx, cy0); ctx.lineTo(gx, cy0 + ch); ctx.stroke();
    var gy = cy0 + ch * k / 3;
    ctx.beginPath(); ctx.moveTo(cx0, gy); ctx.lineTo(cx0 + cw, gy); ctx.stroke();
  }
  document.getElementById('ang').textContent = angleDeg.toFixed(1) + '°';
  requestAnimationFrame(frame);
}
frame();

Straightening is just rotating the whole photo a few degrees around the canvas center — pick a horizon or a building's vertical edge as reference and rotate by the opposite of its tilt. Rotating pulls all four corners outside the canvas, so a crop that trims away those empty triangles always comes with it. This demo animates the rotation angle and shows the crop rectangle automatically shrinking inward as the tilt changes, so no empty corner is ever visible.

It's usually the very first fix — when the horizon or a shoreline came out slightly tilted at capture, or when the frame just needs recomposing to drop unneeded background and land the subject on a rule-of-thirds line. Doing it before tone and color edits is generally the right order, since cropping afterward would cut away part of whatever adjustment was already made.

Rotate too far and the usable field of view shrinks fast, because the corners that need trimming grow with the angle — what survives the crop ends up much tighter than expected. A frame with more than one "horizon" (a sea horizon plus a building's window frame at a different angle) is also easy to miss — leveling one leaves the other still visibly tilted.

When to use

Do this first when the horizon is off or the frame needs recomposing. Rotate too far and the field of view left after cropping shrinks fast.