mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 20:02:20 +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
@@ -63,6 +63,11 @@ import {
|
||||
renderPlacedLayer,
|
||||
transformPlacedData,
|
||||
} from '../placed-layer.js';
|
||||
import {
|
||||
isPerspectiveActive,
|
||||
keystoneCorners,
|
||||
perspectiveWarpCanvas,
|
||||
} from '../perspective-warp.js';
|
||||
|
||||
export function createTransformSession({
|
||||
activeLayer, saveState, composite, fitZoom, drawTransformHandles,
|
||||
@@ -86,6 +91,10 @@ export function createTransformSession({
|
||||
'#ge-transform-w': state.transformPendingFlipH ? -state.transformPendingW : state.transformPendingW,
|
||||
'#ge-transform-h': state.transformPendingFlipV ? -state.transformPendingH : state.transformPendingH,
|
||||
'#ge-transform-rot': state.transformPendingRot,
|
||||
'#ge-transform-skew-x': state.transformPendingSkewX,
|
||||
'#ge-transform-skew-y': state.transformPendingSkewY,
|
||||
'#ge-transform-persp-x': state.transformPendingPerspX,
|
||||
'#ge-transform-persp-y': state.transformPendingPerspY,
|
||||
};
|
||||
for (const [selector, value] of Object.entries(values)) {
|
||||
const input = pop.querySelector(selector);
|
||||
@@ -156,6 +165,10 @@ export function createTransformSession({
|
||||
state.transformPendingRot = 0;
|
||||
state.transformPendingFlipH = false;
|
||||
state.transformPendingFlipV = false;
|
||||
state.transformPendingSkewX = 0;
|
||||
state.transformPendingSkewY = 0;
|
||||
state.transformPendingPerspX = 0;
|
||||
state.transformPendingPerspY = 0;
|
||||
// Snapshot the layer so live preview can re-derive from the
|
||||
// original pixels on every keystroke instead of stacking
|
||||
// destructive edits.
|
||||
@@ -221,12 +234,37 @@ export function createTransformSession({
|
||||
}
|
||||
|
||||
function closeTransformPopup() {
|
||||
if (mobileBoundsHandler) {
|
||||
window.removeEventListener('resize', mobileBoundsHandler);
|
||||
mobileBoundsHandler = null;
|
||||
}
|
||||
if (state.transformPopup) {
|
||||
try { state.transformPopup.remove(); } catch {}
|
||||
state.transformPopup = null;
|
||||
}
|
||||
}
|
||||
|
||||
let mobileBoundsHandler = null;
|
||||
|
||||
// On phones the Transform popup docks at the TOP of the viewport
|
||||
// (see the mobile stylesheet), where its full content height would
|
||||
// otherwise reach down over the transform frame and swallow the
|
||||
// touch targets for the top/left/right grab handles. Cap its height
|
||||
// so its bottom edge stays clear of the canvas top; the body then
|
||||
// scrolls. Desktop keeps the anchored layout untouched.
|
||||
function applyMobileTransformBounds(pop) {
|
||||
if (!pop || !window.matchMedia('(max-width: 820px)').matches) return;
|
||||
const canvas = state.mainCanvas;
|
||||
if (!canvas) return;
|
||||
const POP_TOP = 32; // keep in sync with the mobile stylesheet
|
||||
const GAP = 12;
|
||||
const canvasTop = canvas.getBoundingClientRect().top;
|
||||
const available = Math.round(canvasTop - POP_TOP - GAP);
|
||||
const cap = available > 140 ? available : Math.round(window.innerHeight * 0.42);
|
||||
pop.style.setProperty('min-height', '0', 'important');
|
||||
pop.style.setProperty('max-height', cap + 'px', 'important');
|
||||
}
|
||||
|
||||
// Floating Transform popup — horizontal layout, draggable via its
|
||||
// header, anchored over the right panel (layers area) by default
|
||||
// so it doesn't cover the canvas. Lets the user type exact W/H/Rot
|
||||
@@ -253,6 +291,10 @@ export function createTransformSession({
|
||||
const rotInput = pop.querySelector('#ge-transform-rot');
|
||||
const xInput = pop.querySelector('#ge-transform-x');
|
||||
const yInput = pop.querySelector('#ge-transform-y');
|
||||
const skewXInput = pop.querySelector('#ge-transform-skew-x');
|
||||
const skewYInput = pop.querySelector('#ge-transform-skew-y');
|
||||
const perspXInput = pop.querySelector('#ge-transform-persp-x');
|
||||
const perspYInput = pop.querySelector('#ge-transform-persp-y');
|
||||
const aspectBtn = pop.querySelector('#ge-transform-aspect');
|
||||
const preserveSource = pop.querySelector('#ge-transform-preserve-source');
|
||||
const preserveField = pop.querySelector('.ge-transform-preserve-field');
|
||||
@@ -296,6 +338,10 @@ export function createTransformSession({
|
||||
const rot = parseInt(rotInput.value, 10) || 0;
|
||||
const x = Number.parseFloat(xInput.value);
|
||||
const y = Number.parseFloat(yInput.value);
|
||||
const skewX = Number.parseFloat(skewXInput?.value) || 0;
|
||||
const skewY = Number.parseFloat(skewYInput?.value) || 0;
|
||||
const perspX = Number.parseFloat(perspXInput?.value) || 0;
|
||||
const perspY = Number.parseFloat(perspYInput?.value) || 0;
|
||||
state.transformPendingFlipH = w < 0;
|
||||
state.transformPendingFlipV = h < 0;
|
||||
w = Math.abs(w || state.transformOrigW);
|
||||
@@ -303,6 +349,10 @@ export function createTransformSession({
|
||||
state.transformPendingW = Math.max(1, w);
|
||||
state.transformPendingH = Math.max(1, h);
|
||||
state.transformPendingRot = rot;
|
||||
state.transformPendingSkewX = skewX;
|
||||
state.transformPendingSkewY = skewY;
|
||||
state.transformPendingPerspX = perspX;
|
||||
state.transformPendingPerspY = perspY;
|
||||
if (Number.isFinite(x) && Number.isFinite(y)) state.transformCenter = { x, y };
|
||||
reapplyTransform();
|
||||
};
|
||||
@@ -335,6 +385,10 @@ export function createTransformSession({
|
||||
refresh();
|
||||
});
|
||||
rotInput.addEventListener('input', refresh);
|
||||
skewXInput?.addEventListener('input', refresh);
|
||||
skewYInput?.addEventListener('input', refresh);
|
||||
perspXInput?.addEventListener('input', refresh);
|
||||
perspYInput?.addEventListener('input', refresh);
|
||||
aspectBtn.addEventListener('click', () => {
|
||||
state.transformAspectLock = !state.transformAspectLock;
|
||||
aspectBtn.classList.toggle('active', state.transformAspectLock);
|
||||
@@ -384,6 +438,9 @@ export function createTransformSession({
|
||||
});
|
||||
});
|
||||
attachSpinRepeat(pop);
|
||||
applyMobileTransformBounds(pop);
|
||||
mobileBoundsHandler = () => applyMobileTransformBounds(pop);
|
||||
window.addEventListener('resize', mobileBoundsHandler);
|
||||
}
|
||||
|
||||
// Header-drag for the Transform popup. Default position: over the
|
||||
@@ -494,6 +551,14 @@ export function createTransformSession({
|
||||
ctx.save();
|
||||
ctx.translate(canvas.width / 2, canvas.height / 2);
|
||||
if (geometry.rotation) ctx.rotate(geometry.radians);
|
||||
if (isPerspectiveActive(geometry.perspectiveX, geometry.perspectiveY)) {
|
||||
const warped = perspectiveWarpCanvas(source, keystoneCorners(geometry.perspectiveX, geometry.perspectiveY), canvas.width, canvas.height);
|
||||
ctx.scale(Math.sign(geometry.signedScaleX) || 1, Math.sign(geometry.signedScaleY) || 1);
|
||||
ctx.drawImage(warped, -warped.width / 2, -warped.height / 2);
|
||||
ctx.restore();
|
||||
return canvas;
|
||||
}
|
||||
if (geometry.skewX || geometry.skewY) ctx.transform(1, Math.tan((geometry.skewY || 0) * Math.PI / 180), Math.tan((geometry.skewX || 0) * Math.PI / 180), 1, 0, 0);
|
||||
ctx.scale(geometry.signedScaleX, geometry.signedScaleY);
|
||||
ctx.drawImage(source, -snapshot.width / 2, -snapshot.height / 2, snapshot.width, snapshot.height);
|
||||
ctx.restore();
|
||||
@@ -507,6 +572,10 @@ export function createTransformSession({
|
||||
rotation: state.transformPendingRot,
|
||||
flipH: state.transformPendingFlipH,
|
||||
flipV: state.transformPendingFlipV,
|
||||
skewX: state.transformPendingSkewX,
|
||||
skewY: state.transformPendingSkewY,
|
||||
perspectiveX: state.transformPendingPerspX,
|
||||
perspectiveY: state.transformPendingPerspY,
|
||||
centerX: state.transformCenter?.x,
|
||||
centerY: state.transformCenter?.y,
|
||||
};
|
||||
@@ -519,6 +588,10 @@ export function createTransformSession({
|
||||
state.transformPendingRot = target.rotation;
|
||||
state.transformPendingFlipH = target.flipH;
|
||||
state.transformPendingFlipV = target.flipV;
|
||||
state.transformPendingSkewX = target.skewX || 0;
|
||||
state.transformPendingSkewY = target.skewY || 0;
|
||||
state.transformPendingPerspX = target.perspectiveX || 0;
|
||||
state.transformPendingPerspY = target.perspectiveY || 0;
|
||||
state.transformCenter = { x: target.centerX, y: target.centerY };
|
||||
}
|
||||
|
||||
@@ -714,6 +787,10 @@ export function createTransformSession({
|
||||
state.transformPendingRot = 0;
|
||||
state.transformPendingFlipH = false;
|
||||
state.transformPendingFlipV = false;
|
||||
state.transformPendingSkewX = 0;
|
||||
state.transformPendingSkewY = 0;
|
||||
state.transformPendingPerspX = 0;
|
||||
state.transformPendingPerspY = 0;
|
||||
state.transformActive = false;
|
||||
state.transformTarget = null;
|
||||
state.transformLayer = null;
|
||||
|
||||
Reference in New Issue
Block a user