PageSourceSearch

https://odysseus-lxcq.onrender.com/static/js/editor/snap.js

js odysseus-lxcq.onrender.com collected 2026-10-03 11:44:51 UTC 3,863 bytes, 109 lines download raw bytes

1/**
2 * Snap-while-dragging: when the move tool drags a layer near another
3 * layer's edge or the canvas centre/edges, gently lock the proposed
4 * (nx, ny) to the nearest target within SNAP_PX.
5 *
6 * The implementation is pure — it takes the layer being moved + the
7 * trial offset + a context describing zoom + the other layers, and
8 * returns the snapped position plus any guides to draw.
9 *
10 * The legacy gallery editor's `_computeSnap` is a one-line wrapper
11 * that builds the context from module state.
12 *
13 * @param {{canvas: HTMLCanvasElement, id: string}} layer
14 *   The layer currently being moved.
15 * @param {number} nx / ny
16 *   Trial offset (top-left) in canvas pixels, before snapping.
17 * @param {{
18 *   zoom:        number,
19 *   canvasW:     number,
20 *   canvasH:     number,
21 *   otherLayers: Array<{visible: boolean, id: string, canvas: HTMLCanvasElement, offset: {x:number, y:number}}>,
22 * }} ctx
23 * @returns {{x: number, y: number, guides: Array}}
24 */
25export function computeSnap(layer, nx, ny, ctx) {
26  if (!layer || !layer.canvas || !ctx) return { x: nx, y: ny, guides: [] };
27  const zoom = Number.isFinite(Number(ctx.zoom)) ? Number(ctx.zoom) : 1;
28  const SNAP_PX = 6 / Math.max(zoom, 0.0001);
29  const cw = Number(ctx.canvasW) || 0, ch = Number(ctx.canvasH) || 0;
30  const w = layer.canvas.width, h = layer.canvas.height;
31
32  const vTargets = [
33    { x: 0, label: 'canvas-l' },
34    { x: cw, label: 'canvas-r' },
35    { x: cw / 2, label: 'canvas-cx' },
36  ];
37  const hTargets = [
38    { y: 0, label: 'canvas-t' },
39    { y: ch, label: 'canvas-b' },
40    { y: ch / 2, label: 'canvas-cy' },
41  ];
42  const otherLayers = Array.isArray(ctx.otherLayers) ? ctx.otherLayers : [];
43  for (const other of otherLayers) {
44    if (!other.visible || other.id === layer.id) continue;
45    const o = other.offset || { x: 0, y: 0 };
46    const ow = other.canvas.width, oh = other.canvas.height;
47    vTargets.push({ x: o.x,            label: 'layer-l' });
48    vTargets.push({ x: o.x + ow,       label: 'layer-r' });
49    vTargets.push({ x: o.x + ow / 2,   label: 'layer-cx' });
50    hTargets.push({ y: o.y,            label: 'layer-t' });
51    hTargets.push({ y: o.y + oh,       label: 'layer-b' });
52    hTargets.push({ y: o.y + oh / 2,   label: 'layer-cy' });
53  }
54
55  const myEdgesX = { l: nx, cx: nx + w / 2, r: nx + w };
56  const myEdgesY = { t: ny, cy: ny + h / 2, b: ny + h };
57  let bestX = null, bestDx = Infinity;
58  let bestY = null, bestDy = Infinity;
59  for (const [src, val] of Object.entries(myEdgesX)) {
60    for (const t of vTargets) {
61      const d = Math.abs(t.x - val);
62      if (d < SNAP_PX && d < bestDx) {
63        bestDx = d;
64        bestX = { snapTo: t.x, src, target: t };
65      }
66    }
67  }
68  for (const [src, val] of Object.entries(myEdgesY)) {
69    for (const t of hTargets) {
70      const d = Math.abs(t.y - val);
71      if (d < SNAP_PX && d < bestDy) {
72        bestDy = d;
73        bestY = { snapTo: t.y, src, target: t };
74      }
75    }
76  }
77
78  const guides = [];
79  let snappedX = nx, snappedY = ny;
80  if (bestX) {
81    if (bestX.src === 'l') snappedX = bestX.snapTo;
82    else if (bestX.src === 'cx') snappedX = bestX.snapTo - w / 2;
83    else snappedX = bestX.snapTo - w;
84    guides.push({ vertical: true, x: bestX.snapTo });
85  }
86  if (bestY) {
87    if (bestY.src === 't') snappedY = bestY.snapTo;
88    else if (bestY.src === 'cy') snappedY = bestY.snapTo - h / 2;
89    else snappedY = bestY.snapTo - h;
90    guides.push({ vertical: false, y: bestY.snapTo });
91  }
92  return { x: snappedX, y: snappedY, guides };
93}
94
95
96/**
97 * CSS cursor name for each transform-tool handle.
98 *
99 * @param {'tl'|'tr'|'bl'|'br'|'rot'|string} id
100 * @returns {string}
101 */
102export function cursorForHandle(id) {
103  switch (id) {
104    case 'tl': case 'br': return 'nwse-resize';
105    case 'tr': case 'bl': return 'nesw-resize';
106    case 'rot': return 'grab';
107    default: return 'default';
108  }
109}

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.