PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/tools/crop.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:00:59 UTC 5,630 bytes, 137 lines download raw bytes

1/**
2 * Crop tool — drag-rect selection that lets the user cut down the
3 * canvas to a smaller region. Supports Shift-lock aspect ratio and
4 * click-inside-rect to reposition an existing crop without redrawing.
5 *
6 * Owns its own begin/drag/end handlers and reads/writes shared state.
7 * The factory takes a small dependency bag for things still living in
8 * galleryEditor.js — `composite` redraws the canvas, `showCropApply`
9 * mounts the floating W×H + Apply panel after the user finishes
10 * dragging.
11 *
12 * @param {{
13 *   composite: () => void,
14 *   showCropApply: () => void,
15 * }} deps
16 */
17import { state } from '../state.js';
18import { canvasCoords } from '../canvas-coords.js';
19import { drawCheckerboard } from '../checkerboard.js';
20
21export function createCropTool({ composite, showCropApply }) {
22  return {
23    begin(e) {
24      const coords = canvasCoords(e, state.mainCanvas);
25      // Click inside an existing crop rect → switch to move-mode so
26      // the user can reposition without redrawing.
27      if (state.cropRect &&
28          coords.x >= state.cropRect.x && coords.x <= state.cropRect.x + state.cropRect.w &&
29          coords.y >= state.cropRect.y && coords.y <= state.cropRect.y + state.cropRect.h) {
30        state.cropMoving = true;
31        state.cropMoveStart = { x: coords.x, y: coords.y, rx: state.cropRect.x, ry: state.cropRect.y };
32        return;
33      }
34      state.cropping = true;
35      state.cropStart = coords;
36      state.cropEnd = { ...state.cropStart };
37      state.cropRect = null;
38      state.cropAspectLock = null;
39      // Tear down the size panel while the user is drawing a new rect.
40      const old = state.container?.querySelector('.ge-crop-apply');
41      if (old) old.remove();
42    },
43
44    drag(e) {
45      // Move-mode: drag the existing rect around the canvas.
46      if (state.cropMoving && state.cropRect && state.cropMoveStart) {
47        e.preventDefault();
48        const c = canvasCoords(e, state.mainCanvas);
49        const dx = c.x - state.cropMoveStart.x;
50        const dy = c.y - state.cropMoveStart.y;
51        let nx = state.cropMoveStart.rx + dx;
52        let ny = state.cropMoveStart.ry + dy;
53        // Clamp to canvas bounds so the rect stays fully visible.
54        nx = Math.max(0, Math.min(nx, state.mainCanvas.width - state.cropRect.w));
55        ny = Math.max(0, Math.min(ny, state.mainCanvas.height - state.cropRect.h));
56        state.cropRect = { ...state.cropRect, x: nx, y: ny };
57        composite();
58        return;
59      }
60      if (!state.cropping) return;
61      e.preventDefault();
62      state.cropEnd = canvasCoords(e, state.mainCanvas);
63      // Shift-held = lock aspect ratio. First Shift press during the
64      // drag snapshots the current aspect; subsequent moves stay locked.
65      // Releasing Shift resets so the user can re-lock at a new ratio.
66      if (e.shiftKey) {
67        const rawDx = state.cropEnd.x - state.cropStart.x;
68        const rawDy = state.cropEnd.y - state.cropStart.y;
69        if (state.cropAspectLock == null) {
70          const rawW = Math.abs(rawDx) || 1;
71          const rawH = Math.abs(rawDy) || 1;
72          state.cropAspectLock = rawW / rawH;
73        }
74        const absDx = Math.abs(rawDx);
75        const absDy = Math.abs(rawDy);
76        // Whichever axis the user moved more (relative to the lock) is
77        // the driver; scale the other to preserve aspect.
78        let dx, dy;
79        if (absDx >= absDy * state.cropAspectLock) {
80          dx = rawDx;
81          dy = Math.sign(rawDy || 1) * (absDx / state.cropAspectLock);
82        } else {
83          dy = rawDy;
84          dx = Math.sign(rawDx || 1) * (absDy * state.cropAspectLock);
85        }
86        state.cropEnd = { x: state.cropStart.x + dx, y: state.cropStart.y + dy };
87      } else {
88        state.cropAspectLock = null;
89      }
90      composite();
91      // Draw crop overlay.
92      const x = Math.min(state.cropStart.x, state.cropEnd.x);
93      const y = Math.min(state.cropStart.y, state.cropEnd.y);
94      const w = Math.abs(state.cropEnd.x - state.cropStart.x);
95      const h = Math.abs(state.cropEnd.y - state.cropStart.y);
96      state.mainCtx.fillStyle = 'rgba(0,0,0,0.4)';
97      state.mainCtx.fillRect(0, 0, state.mainCanvas.width, state.mainCanvas.height);
98      state.mainCtx.clearRect(x, y, w, h);
99      // Redraw layers inside the crop rect (dim everything outside).
100      state.mainCtx.save();
101      state.mainCtx.beginPath();
102      state.mainCtx.rect(x, y, w, h);
103      state.mainCtx.clip();
104      drawCheckerboard(state.mainCtx, state.mainCanvas.width, state.mainCanvas.height);
105      for (const layer of state.layers) {
106        if (!layer.visible) continue;
107        state.mainCtx.globalAlpha = layer.opacity;
108        const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
109        state.mainCtx.drawImage(layer.canvas, off.x, off.y);
110      }
111      state.mainCtx.globalAlpha = 1;
112      state.mainCtx.restore();
113      // Dashed border around the kept region.
114      state.mainCtx.strokeStyle = '#fff';
115      state.mainCtx.lineWidth = 1;
116      state.mainCtx.setLineDash([4, 4]);
117      state.mainCtx.strokeRect(x, y, w, h);
118      state.mainCtx.setLineDash([]);
119      state.cropRect = { x, y, w, h };
120    },
121
122    end() {
123      // Move-mode wrap-up: refresh the floating panel so Apply follows
124      // the rect to its new spot.
125      if (state.cropMoving) {
126        state.cropMoving = false;
127        state.cropMoveStart = null;
128        if (state.cropRect) showCropApply();
129        return;
130      }
131      state.cropping = false;
132      if (state.cropRect && state.cropRect.w > 5 && state.cropRect.h > 5) {
133        showCropApply();
134      }
135    },
136  };
137}

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.