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
+49
View File
@@ -0,0 +1,49 @@
/** Shape boolean operations via canvas compositing. */
export const SHAPE_BOOLEAN_OPS = Object.freeze(['unite', 'subtract', 'intersect', 'exclude']);
const OP_COMPOSITE = Object.freeze({
unite: 'source-over',
subtract: 'destination-out',
intersect: 'destination-in',
exclude: 'xor',
});
/**
* Combine layer canvases into a single document-sized canvas.
* @param {Array<{canvas: HTMLCanvasElement}>} layers bottom-to-top order
* @param {string} op one of SHAPE_BOOLEAN_OPS
* @param {{width:number, height:number, offsets?: Array<{x:number,y:number}>}} options
* @returns {HTMLCanvasElement|null}
*/
export function combineLayers(layers, op, { width = 0, height = 0, offsets = [] } = {}) {
const list = Array.isArray(layers) ? layers.filter(layer => layer && layer.canvas) : [];
if (!list.length) return null;
const mode = SHAPE_BOOLEAN_OPS.includes(op) ? op : 'unite';
const canvas = document.createElement('canvas');
canvas.width = Math.max(1, Math.round(width) || list[0].canvas.width);
canvas.height = Math.max(1, Math.round(height) || list[0].canvas.height);
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const base = list[0];
const baseOffset = offsets[0] || { x: 0, y: 0 };
if (mode === 'unite') {
ctx.globalCompositeOperation = 'source-over';
list.forEach((layer, index) => {
const offset = offsets[index] || { x: 0, y: 0 };
ctx.drawImage(layer.canvas, offset.x, offset.y);
});
} else {
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(base.canvas, baseOffset.x, baseOffset.y);
ctx.globalCompositeOperation = OP_COMPOSITE[mode];
for (let index = 1; index < list.length; index += 1) {
const layer = list[index];
const offset = offsets[index] || { x: 0, y: 0 };
ctx.drawImage(layer.canvas, offset.x, offset.y);
}
}
ctx.globalCompositeOperation = 'source-over';
return canvas;
}