PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/canvas-transforms.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:07 UTC 5,551 bytes, 132 lines download raw bytes

1/**
2 * Whole-document transforms: rotate by 90/180/270° or flip horizontal/
3 * vertical. These mutate every layer's canvas + the offset map + the
4 * document's overall width/height so the result feels like the whole
5 * image rotated as one piece.
6 *
7 * Pure-ish — reads/writes shared state directly; the factory takes a
8 * small dep bag for the orchestration plumbing (undo snapshot, canvas
9 * loading overlay, fit-zoom-to-viewport, composite redraw).
10 *
11 * @param {{
12 *   saveState:           (label?: string) => void,
13 *   composite:           () => void,
14 *   fitZoom:             () => void,
15 *   showCanvasLoading:   (label: string) => void,
16 *   hideCanvasLoading:   () => void,
17 * }} deps
18 */
19import { state } from './state.js';
20
21export function createCanvasTransforms({ saveState, composite, fitZoom, showCanvasLoading, hideCanvasLoading }) {
22  return {
23    /**
24     * Rotate the entire document by `deg` (90 / 180 / 270). 90 and 270
25     * swap canvas dimensions. Each layer is rotated around its own
26     * centre, then its centre is rotated around the old image centre
27     * and translated into the new image's frame.
28     *
29     * Wrapped in requestAnimationFrame because the rotation pass can
30     * block the UI for 0.5–2 s on big images — the spinner overlay
31     * paints before we block.
32     */
33    rotateAll(deg) {
34      if (!state.layers.length) return;
35      saveState(`Rotate ${deg}°`);
36      showCanvasLoading('Rotating…');
37      const oldW = state.imgWidth, oldH = state.imgHeight;
38      const swap = (deg === 90 || deg === 270);
39      const newW = swap ? oldH : oldW;
40      const newH = swap ? oldW : oldH;
41      const rad = (deg * Math.PI) / 180;
42      const cos = Math.cos(rad), sin = Math.sin(rad);
43      requestAnimationFrame(() => {
44        try {
45          for (const layer of state.layers) {
46            const lw = layer.canvas.width, lh = layer.canvas.height;
47            const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
48            // Layer centre in old image coords.
49            const cx = off.x + lw / 2;
50            const cy = off.y + lh / 2;
51            // Rotate the centre around the old image centre and
52            // translate so the new image centre lands at (newW/2, newH/2).
53            const dx = cx - oldW / 2;
54            const dy = cy - oldH / 2;
55            const nx = dx * cos - dy * sin + newW / 2;
56            const ny = dx * sin + dy * cos + newH / 2;
57            // New per-layer dims: swap when 90/270.
58            const newLw = swap ? lh : lw;
59            const newLh = swap ? lw : lh;
60            const tmp = document.createElement('canvas');
61            tmp.width = newLw; tmp.height = newLh;
62            const tctx = tmp.getContext('2d');
63            tctx.translate(newLw / 2, newLh / 2);
64            tctx.rotate(rad);
65            tctx.drawImage(layer.canvas, -lw / 2, -lh / 2);
66            layer.canvas.width = newLw;
67            layer.canvas.height = newLh;
68            layer.ctx.drawImage(tmp, 0, 0);
69            // The adjustment-render caches are keyed only by the adjustment
70            // signature, which rotation doesn't change — so composite would draw
71            // the STALE pre-rotation cache (the "had to click twice" bug). Drop
72            // them so the next composite re-renders from the rotated canvas.
73            layer._adjCacheKey = null;
74            layer._adjFinalKey = null;
75            state.layerOffsets.set(layer.id, {
76              x: Math.round(nx - newLw / 2),
77              y: Math.round(ny - newLh / 2),
78            });
79          }
80          state.imgWidth = newW;
81          state.imgHeight = newH;
82          state.mainCanvas.width = newW;
83          state.mainCanvas.height = newH;
84          if (state.maskCanvas) {
85            state.maskCanvas.width = newW;
86            state.maskCanvas.height = newH;
87          }
88          const sizeLabel = document.getElementById('ge-canvas-size');
89          if (sizeLabel) sizeLabel.textContent = `${newW}×${newH}`;
90          fitZoom();
91          composite();
92        } finally {
93          hideCanvasLoading();
94        }
95      });
96    },
97
98    /**
99     * Mirror every layer horizontally ('h') or vertically ('v').
100     * Canvas dimensions don't change. Each layer offset is reflected
101     * around the image centre.
102     */
103    flipAll(axis) {
104      if (!state.layers.length) return;
105      saveState(axis === 'h' ? 'Flip horizontal' : 'Flip vertical');
106      for (const layer of state.layers) {
107        const lw = layer.canvas.width, lh = layer.canvas.height;
108        const tmp = document.createElement('canvas');
109        tmp.width = lw; tmp.height = lh;
110        const tctx = tmp.getContext('2d');
111        tctx.save();
112        if (axis === 'h') { tctx.translate(lw, 0); tctx.scale(-1, 1); }
113        else              { tctx.translate(0, lh); tctx.scale(1, -1); }
114        tctx.drawImage(layer.canvas, 0, 0);
115        tctx.restore();
116        layer.ctx.clearRect(0, 0, lw, lh);
117        layer.ctx.drawImage(tmp, 0, 0);
118        // Invalidate the adjustment-render caches (keyed by adjustment sig only)
119        // so composite redraws from the flipped canvas, not a stale cache.
120        layer._adjCacheKey = null;
121        layer._adjFinalKey = null;
122        const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
123        if (axis === 'h') {
124          state.layerOffsets.set(layer.id, { x: state.imgWidth - off.x - lw, y: off.y });
125        } else {
126          state.layerOffsets.set(layer.id, { x: off.x, y: state.imgHeight - off.y - lh });
127        }
128      }
129      composite();
130    },
131  };
132}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.