feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)

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>
This commit is contained in:
pewdiepie-archdaemon
2026-10-15 11:57:35 +00:00
co-authored by Claude Opus 5.5
parent c74f475679
commit 5fac693c1c
290 changed files with 34358 additions and 766 deletions
+35 -5
View File
@@ -5,6 +5,24 @@ function hexByte(value) {
return Math.max(0, Math.min(255, value)).toString(16).padStart(2, '0');
}
// Average an N-by-N block centred on (cx, cy), clamped to the canvas edges.
// Photoshop's "Sample Size" option: 1 = Point Sample, 3/5/11/31/51 = N-by-N.
function averageBlock(ctx, cx, cy, size, width, height) {
const half = Math.floor(size / 2);
const x0 = Math.max(0, cx - half);
const y0 = Math.max(0, cy - half);
const x1 = Math.min(width - 1, cx + half);
const y1 = Math.min(height - 1, cy + half);
const w = x1 - x0 + 1;
const h = y1 - y0 + 1;
if (w <= 0 || h <= 0) return null;
const data = ctx.getImageData(x0, y0, w, h).data;
let r = 0, g = 0, b = 0, a = 0;
const n = data.length / 4;
for (let i = 0; i < data.length; i += 4) { r += data[i]; g += data[i + 1]; b += data[i + 2]; a += data[i + 3]; }
return { r: Math.round(r / n), g: Math.round(g / n), b: Math.round(b / n), a: a / n };
}
export function createEyedropperTool({ activeLayer, composite, syncColor, syncPreview, showToast }) {
function sample(e, refreshComposite = false) {
const point = canvasCoords(e, state.mainCanvas);
@@ -21,11 +39,23 @@ export function createEyedropperTool({ activeLayer, composite, syncColor, syncPr
} else if (refreshComposite) {
composite();
}
if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) return null;
const pixel = canvas.getContext('2d').getImageData(Math.floor(x), Math.floor(y), 1, 1).data;
const rgb = [pixel[0], pixel[1], pixel[2]];
if (!pixel[3]) return { color: null, rgb, canvas, x, y };
return { color: `#${hexByte(pixel[0])}${hexByte(pixel[1])}${hexByte(pixel[2])}`, rgb, canvas, x, y };
const ix = Math.floor(x);
const iy = Math.floor(y);
if (ix < 0 || iy < 0 || ix >= canvas.width || iy >= canvas.height) return null;
const ctx = canvas.getContext('2d');
const size = Math.max(1, Math.round(Number(state.eyedropperSampleSize) || 1));
let r, g, b, a;
if (size <= 1) {
const pixel = ctx.getImageData(ix, iy, 1, 1).data;
r = pixel[0]; g = pixel[1]; b = pixel[2]; a = pixel[3];
} else {
const avg = averageBlock(ctx, ix, iy, size, canvas.width, canvas.height);
if (!avg) return null;
r = avg.r; g = avg.g; b = avg.b; a = avg.a;
}
const rgb = [r, g, b];
if (!a) return { color: null, rgb, canvas, x, y };
return { color: `#${hexByte(r)}${hexByte(g)}${hexByte(b)}`, rgb, canvas, x, y };
}
return {
preview(e) {