mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 11:52:25 +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>
112 lines
4.0 KiB
JavaScript
112 lines
4.0 KiB
JavaScript
/**
|
||
* Whole-document transforms: rotate by 90/180/270° or flip horizontal/
|
||
* vertical. These mutate every layer's canvas + the offset map + the
|
||
* document's overall width/height so the result feels like the whole
|
||
* image rotated as one piece.
|
||
*
|
||
* Pure-ish — reads/writes shared state directly; the factory takes a
|
||
* small dep bag for the orchestration plumbing (undo snapshot, canvas
|
||
* loading overlay, fit-zoom-to-viewport, composite redraw).
|
||
*
|
||
* @param {{
|
||
* saveState: (label?: string) => void,
|
||
* composite: () => void,
|
||
* fitZoom: () => void,
|
||
* showCanvasLoading: (label: string) => void,
|
||
* hideCanvasLoading: () => void,
|
||
* renderLayerPanel: () => void,
|
||
* }} deps
|
||
*/
|
||
import { state } from './state.js';
|
||
import { flipDocument, rotateDocument, rotateDocumentArbitrary, flipLayer as flipLayerGeometry, rotateLayer as rotateLayerGeometry } from './document-geometry.js';
|
||
|
||
export function createCanvasTransforms({
|
||
saveState, composite, fitZoom, showCanvasLoading, hideCanvasLoading, renderLayerPanel,
|
||
}) {
|
||
return {
|
||
/**
|
||
* Rotate the entire document by `deg` (90 / 180 / 270). 90 and 270
|
||
* swap canvas dimensions. Each layer is rotated around its own
|
||
* centre, then its centre is rotated around the old image centre
|
||
* and translated into the new image's frame.
|
||
*
|
||
* Wrapped in requestAnimationFrame because the rotation pass can
|
||
* block the UI for 0.5–2 s on big images — the spinner overlay
|
||
* paints before we block.
|
||
*/
|
||
rotateAll(deg) {
|
||
if (!state.layers.length) return;
|
||
saveState(`Rotate ${deg}°`);
|
||
showCanvasLoading('Rotating…');
|
||
requestAnimationFrame(() => {
|
||
try {
|
||
const result = rotateDocument(state, deg);
|
||
const sizeLabel = document.getElementById('ge-canvas-size');
|
||
if (sizeLabel && result) sizeLabel.textContent = `${result.width}×${result.height}`;
|
||
fitZoom();
|
||
composite();
|
||
renderLayerPanel?.();
|
||
} finally {
|
||
hideCanvasLoading();
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Rotate the entire document by an arbitrary angle in degrees
|
||
* (clockwise; negative for counter-clockwise), expanding the canvas
|
||
* so nothing is clipped. Right angles use the exact integer path.
|
||
*/
|
||
rotateAllArbitrary(deg) {
|
||
if (!state.layers.length) return;
|
||
saveState(`Rotate ${deg}°`);
|
||
showCanvasLoading('Rotating…');
|
||
requestAnimationFrame(() => {
|
||
try {
|
||
const result = rotateDocumentArbitrary(state, deg);
|
||
const sizeLabel = document.getElementById('ge-canvas-size');
|
||
if (sizeLabel && result) sizeLabel.textContent = `${result.width}×${result.height}`;
|
||
fitZoom();
|
||
composite();
|
||
renderLayerPanel?.();
|
||
} finally {
|
||
hideCanvasLoading();
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Mirror every layer horizontally ('h') or vertically ('v').
|
||
* Canvas dimensions don't change. Each layer offset is reflected
|
||
* around the image centre.
|
||
*/
|
||
flipAll(axis) {
|
||
if (!state.layers.length) return;
|
||
saveState(axis === 'h' ? 'Flip horizontal' : 'Flip vertical');
|
||
flipDocument(state, axis);
|
||
composite();
|
||
renderLayerPanel?.();
|
||
},
|
||
|
||
/** Flip only the active layer, in place (Edit ▸ Transform ▸ Flip). */
|
||
flipLayer(axis) {
|
||
const layer = state.layers.find((item) => item.id === state.activeLayerId);
|
||
if (!layer) return;
|
||
saveState(axis === 'h' ? 'Flip layer horizontal' : 'Flip layer vertical');
|
||
flipLayerGeometry(state, layer, axis);
|
||
composite();
|
||
renderLayerPanel?.();
|
||
},
|
||
|
||
/** Rotate only the active layer by 90/180/270°, centre preserved. */
|
||
rotateLayer(deg) {
|
||
const layer = state.layers.find((item) => item.id === state.activeLayerId);
|
||
if (!layer) return;
|
||
saveState(`Rotate layer ${deg}°`);
|
||
rotateLayerGeometry(state, layer, deg);
|
||
composite();
|
||
renderLayerPanel?.();
|
||
},
|
||
};
|
||
}
|