Fix canvas coordinate mismatch and handle jump-on-grab in Grid_Setup

- Canvas width now read via getBoundingClientRect after setting style.width=100%,
  avoiding the parentElement.clientWidth padding issue that made css_w exceed
  the actual rendered width and broke hit-testing
- All handle drags (corners + midpoints) now use relative delta via drag_prev_img
  instead of absolute cursor position, preventing handle teleport on grab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 03:20:09 +00:00
parent ede87bb90f
commit 046fe99c72

View File

@@ -45,32 +45,37 @@ export class Grid_Setup {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
this.#img = img; this.#img = img;
const max_w = this.#canvas.parentElement.clientWidth || 800;
const max_h = Math.floor(window.innerHeight * 0.65);
// Canvas fills the available space // Let CSS determine the width, then read back the actual rendered value.
this.#css_w = max_w; // Using parentElement.clientWidth directly would include the parent's
this.#css_h = max_h; // padding, causing css_w to exceed the real content area and making
// getBoundingClientRect() return a different width than css_w.
this.#canvas.style.width = '100%';
const css_w = this.#canvas.getBoundingClientRect().width || 800;
const css_h = Math.floor(window.innerHeight * 0.65);
this.#css_w = css_w;
this.#css_h = css_h;
// Scale: fit image within canvas with slight padding // Scale: fit image within canvas with slight padding
this.#scale = Math.min( this.#scale = Math.min(
(max_w * 0.9) / img.width, (css_w * 0.9) / img.width,
(max_h * 0.9) / img.height, (css_h * 0.9) / img.height,
); );
const dpr = window.devicePixelRatio || 1; const dpr = window.devicePixelRatio || 1;
this.#canvas.width = this.#css_w * dpr; this.#canvas.width = css_w * dpr;
this.#canvas.height = this.#css_h * dpr; this.#canvas.height = css_h * dpr;
this.#canvas.style.width = this.#css_w + 'px'; this.#canvas.style.width = css_w + 'px';
this.#canvas.style.height = this.#css_h + 'px'; this.#canvas.style.height = css_h + 'px';
this.#ctx.scale(dpr, dpr); this.#ctx.scale(dpr, dpr);
// Camera: start centered, image fitted within canvas // Camera: start centered, image fitted within canvas
const img_w = img.width * this.#scale; const img_w = img.width * this.#scale;
const img_h = img.height * this.#scale; const img_h = img.height * this.#scale;
this.#cam_z = 1; this.#cam_z = 1;
this.#cam_x = (max_w - img_w) / 2; this.#cam_x = (css_w - img_w) / 2;
this.#cam_y = (max_h - img_h) / 2; this.#cam_y = (css_h - img_h) / 2;
// Default corners: 15% inset in image coords // Default corners: 15% inset in image coords
const mx = img.width * 0.15; const mx = img.width * 0.15;
@@ -159,9 +164,7 @@ export class Grid_Setup {
const hit = this.#find_handle(sp); const hit = this.#find_handle(sp);
if (hit !== -1) { if (hit !== -1) {
this.#drag_idx = hit; this.#drag_idx = hit;
if (hit >= 4) { this.#drag_prev_img = this.#world_to_img(this.#to_world(sp));
this.#drag_prev_img = this.#world_to_img(this.#to_world(sp));
}
} else { } else {
this.#panning = true; this.#panning = true;
this.#pan_last = sp; this.#pan_last = sp;
@@ -173,16 +176,17 @@ export class Grid_Setup {
const sp = this.#screen_pos(e); const sp = this.#screen_pos(e);
if (this.#drag_idx !== -1) { if (this.#drag_idx !== -1) {
const img_pos = this.#world_to_img(this.#to_world(sp)); const img_pos = this.#world_to_img(this.#to_world(sp));
const dx = img_pos.x - this.#drag_prev_img.x;
const dy = img_pos.y - this.#drag_prev_img.y;
if (this.#drag_idx < 4) { if (this.#drag_idx < 4) {
this.#corners[this.#drag_idx] = img_pos; const i = this.#drag_idx;
this.#corners[i] = { x: this.#corners[i].x + dx, y: this.#corners[i].y + dy };
} else { } else {
const [a, b] = Grid_Setup.#MIDPOINT_EDGES[this.#drag_idx - 4]; const [a, b] = Grid_Setup.#MIDPOINT_EDGES[this.#drag_idx - 4];
const dx = img_pos.x - this.#drag_prev_img.x;
const dy = img_pos.y - this.#drag_prev_img.y;
this.#corners[a] = { x: this.#corners[a].x + dx, y: this.#corners[a].y + dy }; this.#corners[a] = { x: this.#corners[a].x + dx, y: this.#corners[a].y + dy };
this.#corners[b] = { x: this.#corners[b].x + dx, y: this.#corners[b].y + dy }; this.#corners[b] = { x: this.#corners[b].x + dx, y: this.#corners[b].y + dy };
this.#drag_prev_img = img_pos;
} }
this.#drag_prev_img = img_pos;
this.#draw(); this.#draw();
} else if (this.#panning) { } else if (this.#panning) {
this.#cam_x += sp.x - this.#pan_last.x; this.#cam_x += sp.x - this.#pan_last.x;