mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-21 14:22:19 +02:00
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:
co-authored by
Claude Opus 5.5
parent
c74f475679
commit
5fac693c1c
@@ -4,9 +4,17 @@
|
||||
* mouse-up keeps the selection visible (the panel's action buttons
|
||||
* read `state.lassoPoints` to act on it).
|
||||
*
|
||||
* Magnetic sub-mode: when `state.lassoMagnetic` is on, each sampled
|
||||
* point is snapped to the strongest local image gradient within
|
||||
* `state.lassoMagneticWidth` px, so the outline hugs edges. Edge
|
||||
* strength below `state.lassoMagneticContrast` (0-100) is ignored.
|
||||
*
|
||||
* Owns its own begin/drag/end handlers and reads/writes shared state.
|
||||
*
|
||||
* @param {{
|
||||
* activeLayer: () => object | null,
|
||||
* saveState: (label?: string) => void,
|
||||
* commitSelectionMask: (mask, layer, mode, source, offset) => void,
|
||||
* composite: () => void,
|
||||
* drawLassoOverlay: () => void,
|
||||
* syncToolClearIndicators: () => void,
|
||||
@@ -17,16 +25,69 @@ import { selectionModeForEvent } from '../selection-modifiers.js';
|
||||
import { canvasCoords } from '../canvas-coords.js';
|
||||
import { buildLassoMask } from './lasso-mask.js';
|
||||
|
||||
// Luminance-gradient magnitude field for the active layer, used by the
|
||||
// magnetic sub-mode. Built once per stroke; null when unavailable.
|
||||
function buildEdgeField(layer, width, height) {
|
||||
if (!layer || !layer.ctx || !width || !height) return null;
|
||||
try {
|
||||
const data = layer.ctx.getImageData(0, 0, width, height).data;
|
||||
const lum = new Float32Array(width * height);
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
lum[i] = 0.299 * data[i * 4] + 0.587 * data[i * 4 + 1] + 0.114 * data[i * 4 + 2];
|
||||
}
|
||||
const mag = new Float32Array(width * height);
|
||||
for (let y = 1; y < height - 1; y++) {
|
||||
for (let x = 1; x < width - 1; x++) {
|
||||
const i = y * width + x;
|
||||
const gx = lum[i + 1] - lum[i - 1];
|
||||
const gy = lum[i + width] - lum[i - width];
|
||||
mag[i] = Math.abs(gx) + Math.abs(gy);
|
||||
}
|
||||
}
|
||||
return { mag, width, height };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function snapToEdge(field, x, y, radius, contrast) {
|
||||
const r = Math.max(1, Math.round(radius));
|
||||
const threshold = contrast * 2.55;
|
||||
let best = null;
|
||||
let bestScore = -Infinity;
|
||||
const cx = Math.round(x);
|
||||
const cy = Math.round(y);
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
const px = cx + dx;
|
||||
const py = cy + dy;
|
||||
if (px < 1 || py < 1 || px >= field.width - 1 || py >= field.height - 1) continue;
|
||||
const m = field.mag[py * field.width + px];
|
||||
if (m < threshold) continue;
|
||||
const score = m - Math.hypot(dx, dy) * 2;
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = { x: px, y: py };
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
export function createLassoTool({
|
||||
activeLayer, saveState, commitSelectionMask,
|
||||
composite, drawLassoOverlay, syncToolClearIndicators,
|
||||
}) {
|
||||
let pendingMode = 'replace';
|
||||
let edgeField = null;
|
||||
return {
|
||||
begin(e) {
|
||||
pendingMode = selectionModeForEvent(e, state.wandMode || 'replace');
|
||||
state.lassoPoints = [];
|
||||
state.lassoActive = true;
|
||||
edgeField = state.lassoMagnetic
|
||||
? buildEdgeField(activeLayer?.(), state.imgWidth, state.imgHeight)
|
||||
: null;
|
||||
const coords = canvasCoords(e, state.mainCanvas);
|
||||
state.lassoPoints.push(coords);
|
||||
},
|
||||
@@ -34,7 +95,19 @@ export function createLassoTool({
|
||||
drag(e) {
|
||||
if (!state.lassoActive) return;
|
||||
e.preventDefault();
|
||||
const coords = canvasCoords(e, state.mainCanvas);
|
||||
let coords = canvasCoords(e, state.mainCanvas);
|
||||
if (state.lassoMagnetic && edgeField) {
|
||||
const snapped = snapToEdge(
|
||||
edgeField,
|
||||
coords.x,
|
||||
coords.y,
|
||||
state.lassoMagneticWidth || 12,
|
||||
state.lassoMagneticContrast ?? 20,
|
||||
);
|
||||
if (snapped) coords = snapped;
|
||||
}
|
||||
const last = state.lassoPoints[state.lassoPoints.length - 1];
|
||||
if (last && Math.hypot(coords.x - last.x, coords.y - last.y) < 1) return;
|
||||
state.lassoPoints.push(coords);
|
||||
// Live overlay: dashed white outline + translucent red fill.
|
||||
composite();
|
||||
@@ -57,6 +130,7 @@ export function createLassoTool({
|
||||
|
||||
end() {
|
||||
state.lassoActive = false;
|
||||
edgeField = null;
|
||||
if (state.lassoPoints.length < 3) {
|
||||
state.lassoPoints = [];
|
||||
composite();
|
||||
@@ -86,6 +160,7 @@ export function createLassoTool({
|
||||
cancel() {
|
||||
if (!state.lassoActive && !state.lassoPoints.length) return false;
|
||||
state.lassoActive = false;
|
||||
edgeField = null;
|
||||
state.lassoPoints = [];
|
||||
composite();
|
||||
syncToolClearIndicators();
|
||||
|
||||
Reference in New Issue
Block a user