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:
1 parent
94a3c1ba0e
commit
8ecf3e94a6
6 files changed
+260
-25
No files matched your search
+73
-9
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user