mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 03:42:20 +02:00
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).
Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
3.7 KiB
JavaScript
100 lines
3.7 KiB
JavaScript
/**
|
|
* Perspective Crop — straighten an arbitrary quadrilateral drawn over the image
|
|
* into a rectangular crop, resizing the document to the quad's edge lengths.
|
|
*
|
|
* Pure functions plus one thin canvas adapter. The quad-to-rectangle homography
|
|
* (solveSquareToQuad, from perspective-warp.js) is applied FORWARD so the quad's
|
|
* contents fill the whole output rectangle; the output size comes from the
|
|
* quad's average edge lengths so the straightened result keeps its proportions.
|
|
*/
|
|
import { solveSquareToQuad } from './perspective-warp.js';
|
|
|
|
function dist(a, b) {
|
|
return Math.hypot((a?.x || 0) - (b?.x || 0), (a?.y || 0) - (b?.y || 0));
|
|
}
|
|
|
|
function clampDim(value) {
|
|
return Math.max(1, Math.round(Number(value) || 1));
|
|
}
|
|
|
|
/** Output size for a crop quad (document px): mean of the opposite edge lengths. */
|
|
export function perspectiveCropSize(corners, srcW = 0, srcH = 0) {
|
|
const c = Array.isArray(corners) ? corners : [];
|
|
if (c.length < 4) {
|
|
return { width: clampDim(srcW || 1), height: clampDim(srcH || 1) };
|
|
}
|
|
const [tl, tr, br, bl] = c;
|
|
const width = (dist(tl, tr) + dist(bl, br)) / 2;
|
|
const height = (dist(tl, bl) + dist(tr, br)) / 2;
|
|
return { width: clampDim(width), height: clampDim(height) };
|
|
}
|
|
|
|
/** Normalise document-space corners to source fractions for warpRgba. */
|
|
export function normalizeCropCorners(corners, srcW, srcH) {
|
|
const w = clampDim(srcW), h = clampDim(srcH);
|
|
const c = Array.isArray(corners) ? corners.slice(0, 4) : [];
|
|
while (c.length < 4) c.push({ x: 0, y: 0 });
|
|
return c.map(point => ({
|
|
x: Math.max(0, Math.min(1, (Number(point?.x) || 0) / w)),
|
|
y: Math.max(0, Math.min(1, (Number(point?.y) || 0) / h)),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Straighten the quad in `sourceCanvas` to a `outW` x `outH` canvas.
|
|
* @returns {HTMLCanvasElement}
|
|
*/
|
|
export function perspectiveCropCanvas(sourceCanvas, corners, outW, outH) {
|
|
const w = clampDim(sourceCanvas?.width || 1);
|
|
const h = clampDim(sourceCanvas?.height || 1);
|
|
const width = clampDim(outW);
|
|
const height = clampDim(outH);
|
|
const srcData = sourceCanvas.getContext('2d').getImageData(0, 0, w, h).data;
|
|
const quad = normalizeCropCorners(corners, w, h);
|
|
const [a, b, c, d, e, f, g, hh] = solveSquareToQuad(quad);
|
|
const out = new Uint8ClampedArray(width * height * 4);
|
|
const denomX = width > 1 ? width - 1 : 1;
|
|
const denomY = height > 1 ? height - 1 : 1;
|
|
for (let y = 0; y < height; y += 1) {
|
|
const v = y / denomY;
|
|
for (let x = 0; x < width; x += 1) {
|
|
const u = x / denomX;
|
|
const wgt = g * u + hh * v + 1;
|
|
if (!wgt) continue;
|
|
const s = (a * u + b * v + c) / wgt;
|
|
const t = (d * u + e * v + f) / wgt;
|
|
if (s < 0 || s > 1 || t < 0 || t > 1) continue;
|
|
const sx = Math.min(w - 1, Math.max(0, Math.round(s * (w - 1))));
|
|
const sy = Math.min(h - 1, Math.max(0, Math.round(t * (h - 1))));
|
|
const si = (sy * w + sx) * 4;
|
|
const di = (y * width + x) * 4;
|
|
out[di] = srcData[si];
|
|
out[di + 1] = srcData[si + 1];
|
|
out[di + 2] = srcData[si + 2];
|
|
out[di + 3] = srcData[si + 3];
|
|
}
|
|
}
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const ctx = canvas.getContext('2d');
|
|
const image = ctx.createImageData(width, height);
|
|
image.data.set(out);
|
|
ctx.putImageData(image, 0, 0);
|
|
return canvas;
|
|
}
|
|
|
|
/** Default corner set: inset a little from the document corners. */
|
|
export function defaultPerspectiveCorners(srcW, srcH, inset = 0.06) {
|
|
const w = clampDim(srcW), h = clampDim(srcH);
|
|
const dx = Math.round(w * inset), dy = Math.round(h * inset);
|
|
return [
|
|
{ x: dx, y: dy },
|
|
{ x: w - dx, y: dy },
|
|
{ x: w - dx, y: h - dy },
|
|
{ x: dx, y: h - dy },
|
|
];
|
|
}
|
|
|
|
export default { perspectiveCropSize, normalizeCropCorners, perspectiveCropCanvas, defaultPerspectiveCorners };
|