Arrange tool, and a hold that is easier to find

An Arrange tool marks the corners of every image, Hide and own text
rectangle in blue and moves or resizes them by a plain drag, a press
elsewhere doing nothing.  The hold that does the same with any tool is
shorter (450 ms), with more slack, and shows all four corners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-14 00:01:45 +02:00
1 parent 94a3c1ba0e
commit 8ecf3e94a6
6 files changed
+260 -25

No files matched your search

+73 -9
View File
@@ -107,6 +107,19 @@ function strokeCrisp(context, polylines, color, width) {
context.restore();
}
/**
* Four handle marks, one for each corner of an overlay (the grip and the
* Arrange cues), placed by their class.
* @returns {HTMLElement[]}
*/
function cornerHandles() {
return ['nw', 'ne', 'sw', 'se'].map((corner) => {
const handle = document.createElement('span');
handle.className = `handle ${corner}`;
return handle;
});
}
/**
* Draw the corners of a rectangle as short L-shaped marks.
* @param {CanvasRenderingContext2D} context
@@ -221,13 +234,30 @@ export class Renderer {
* @type {{element: HTMLImageElement, data: Uint8Array | null, hash: string}[]}
*/
this.imageElements = [];
/** The green cue around an image being handled by hand. */
/**
* The green cue around an image, text block or Hide being handled by
* hand: its outline and its four corners, the corner being dragged
* marked (all four since 2026-10-13, so that a hold shows where the
* corners are).
*/
this.grip = document.createElement('div');
this.grip.className = 'image_grip';
this.grip.hidden = true;
this.grip.append(...cornerHandles());
this.pageElement.append(this.grip);
/** @type {{rect: import('./page_model.js').Rectangle, corner: [number, number] | null} | null} */
this.gripShown = null;
/**
* The blue cues of the Arrange tool: the corners of every image, text
* rectangle and Hide that can be moved or resized, and an outline for
* images, whose edges may be invisible on the white page (Andy,
* 2026-10-13: figures on a white ground).
* @type {{rect: import('./page_model.js').Rectangle, kind: 'image' | 'text' | 'hide'}[]}
*/
this.arrangeCues = [];
this.cueElements = document.createElement('div');
this.cueElements.className = 'arrange_cues';
this.pageElement.append(this.cueElements);
/** Canvas pixels per page unit. */
this.scale = 1;
@@ -501,19 +531,52 @@ export class Renderer {
const shown = this.gripShown;
this.grip.hidden = !shown;
if (!shown) return;
const page = this.page;
const style = this.grip.style;
style.left = `${(shown.rect.left - page.left) / page.width * 100}%`;
style.top = `${(shown.rect.top - page.top) / page.height * 100}%`;
style.width = `${shown.rect.width / page.width * 100}%`;
style.height = `${shown.rect.height / page.height * 100}%`;
this.placeOverlay(this.grip, shown.rect);
this.grip.classList.toggle('corner', shown.corner !== null);
if (shown.corner) {
style.setProperty('--corner-x', shown.corner[0] ? '100%' : '0%');
style.setProperty('--corner-y', shown.corner[1] ? '100%' : '0%');
this.grip.style.setProperty('--corner-x', shown.corner[0] ? '100%' : '0%');
this.grip.style.setProperty('--corner-y', shown.corner[1] ? '100%' : '0%');
}
}
/**
* Place an overlay element over a rectangle of the page, in percentages
* of the page, so that it keeps its place at any zoom.
* @param {HTMLElement} el
* @param {import('./page_model.js').Rectangle} rect Page units.
*/
placeOverlay(el, rect) {
const page = this.page;
el.style.left = `${(rect.left - page.left) / page.width * 100}%`;
el.style.top = `${(rect.top - page.top) / page.height * 100}%`;
el.style.width = `${rect.width / page.width * 100}%`;
el.style.height = `${rect.height / page.height * 100}%`;
}
/**
* Show the Arrange tool's cues on these rectangles, or none.
* @param {{rect: import('./page_model.js').Rectangle, kind: 'image' | 'text' | 'hide'}[]} cues
*/
setArrangeCues(cues) {
this.arrangeCues = cues;
this.cueElements.replaceChildren();
for (const cue of cues) {
const el = document.createElement('div');
el.className = `arrange_cue ${cue.kind}`;
el.append(...cornerHandles());
this.cueElements.append(el);
}
this.layoutCues();
}
layoutCues() {
const elements = this.cueElements.children;
this.arrangeCues.forEach((cue, i) => {
const el = /** @type {HTMLElement} */ (elements[i]);
if (el) this.placeOverlay(el, cue.rect);
});
}
/** @returns {number} CORNER_REACH_PX in page units at the present zoom. */
cornerReach() {
return CORNER_REACH_PX / this.cssPixelsPerUnit;
@@ -573,6 +636,7 @@ export class Renderer {
this.pageElement.style.height = `${this.page.height / unitsPerCssPixel}px`;
this.layoutImages();
this.layoutGrip();
this.layoutCues();
// When the page grew to the left or upward, keep what is in view in view.
if (this.shownLeft !== null && this.page.left < this.shownLeft) {
desk.scrollLeft += (this.shownLeft - this.page.left) / unitsPerCssPixel;