notes: fix numerical instability in note positions.
Bug: loading and saving a note without moving it could change the note's position. Moving a note with the keyboard could also cause unintended changes to the note's size or position. This was because of floating point errors when converting from percentage coordinates (floats) to pixel coordinates (integers). Fix: store note coordinates as pixel coordinates instead of as percentage coordinates. Percentages are only used to position notes for display. Also fix it so you can resize rotated notes with the keyboard. This was disabled before.
This commit is contained in:
@@ -19,6 +19,10 @@ class Note {
|
|||||||
static timeouts = [];
|
static timeouts = [];
|
||||||
|
|
||||||
id = null;
|
id = null;
|
||||||
|
x = null;
|
||||||
|
y = null;
|
||||||
|
w = null;
|
||||||
|
h = null;
|
||||||
box = null;
|
box = null;
|
||||||
body = null;
|
body = null;
|
||||||
$note_container = null;
|
$note_container = null;
|
||||||
@@ -89,30 +93,36 @@ class Note {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset the note box placement after the box is dragged or resized. Dragging the note
|
// Reset the note box placement after the box is dragged or resized. Dragging the note
|
||||||
// changes the CSS coordinates to pixels, so we have to convert them back to percentages.
|
// changes the CSS coordinates to pixels, so we have to rescale them and convert back
|
||||||
|
// to percentage coordinates.
|
||||||
on_dragstop() {
|
on_dragstop() {
|
||||||
this.place_note(this.note.x, this.note.y, this.note.width, this.note.height);
|
let x = this.$note_box.position().left / this.note.scale_factor;
|
||||||
|
let y = this.$note_box.position().top / this.note.scale_factor;
|
||||||
|
let w = this.$note_box.width() / this.note.scale_factor;
|
||||||
|
let h = this.$note_box.height() / this.note.scale_factor;
|
||||||
|
|
||||||
|
this.place_note(x, y, w, h);
|
||||||
this.note.body.show();
|
this.note.body.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place the note box using absolute percentage coordinates (floats in the range 0.0..1.0).
|
// Place the note box. The input values are pixel coordinates relative to the full image.
|
||||||
place_note(x, y, w, h) {
|
place_note(x, y, w, h) {
|
||||||
if (this.note.embed && this.note.has_rotation) {
|
if (this.note.embed && this.note.has_rotation) {
|
||||||
let position = this.get_min_max_position();
|
let position = this.get_min_max_position();
|
||||||
x = position.norm_left / this.note.image_width;
|
x = position.norm_left / this.note.scale_factor;
|
||||||
y = position.norm_top / this.note.image_height;
|
y = position.norm_top / this.note.scale_factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
x = clamp(x, 0.0, 1.0);
|
this.note.w = Math.round(clamp(w, Note.MIN_NOTE_SIZE, this.note.post_width));
|
||||||
y = clamp(y, 0.0, 1.0);
|
this.note.h = Math.round(clamp(h, Note.MIN_NOTE_SIZE, this.note.post_height));
|
||||||
w = clamp(w, Note.MIN_NOTE_SIZE / this.note.post_width, 1.0);
|
this.note.x = Math.round(clamp(x, 0, this.note.post_width - this.note.w));
|
||||||
h = clamp(h, Note.MIN_NOTE_SIZE / this.note.post_height, 1.0);
|
this.note.y = Math.round(clamp(y, 0, this.note.post_height - this.note.h));
|
||||||
|
|
||||||
this.$note_box.css({
|
this.$note_box.css({
|
||||||
top: (100 * y) + '%',
|
top: (100 * this.note.y / this.note.post_height) + '%',
|
||||||
left: (100 * x) + '%',
|
left: (100 * this.note.x / this.note.post_width) + '%',
|
||||||
width: (100 * w) + '%',
|
width: (100 * this.note.w / this.note.post_width) + '%',
|
||||||
height: (100 * h) + '%',
|
height: (100 * this.note.h / this.note.post_height) + '%',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,74 +189,50 @@ class Note {
|
|||||||
}
|
}
|
||||||
|
|
||||||
key_nudge(event) {
|
key_nudge(event) {
|
||||||
let $note_box = this.note.box.$note_box;
|
|
||||||
|
|
||||||
let current_top = Math.round($note_box.position().top);
|
|
||||||
let current_left = Math.round($note_box.position().left);
|
|
||||||
|
|
||||||
switch (event.originalEvent.key) {
|
switch (event.originalEvent.key) {
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
current_top--;
|
this.note.y--;
|
||||||
break;
|
break;
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
current_top++;
|
this.note.y++;
|
||||||
break;
|
break;
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
current_left--;
|
this.note.x--;
|
||||||
break;
|
break;
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
current_left++;
|
this.note.x++;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
let position = this.get_min_max_position(current_top, current_left);
|
this.place_note(this.note.x, this.note.y, this.note.w, this.note.h);
|
||||||
$note_box.css({
|
|
||||||
top: position.percent_top,
|
|
||||||
left: position.percent_left,
|
|
||||||
});
|
|
||||||
|
|
||||||
Note.Body.hide_all();
|
Note.Body.hide_all();
|
||||||
$note_box.addClass("unsaved");
|
this.$note_box.addClass("unsaved");
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
key_resize(event) {
|
key_resize(event) {
|
||||||
let $note_box = this.note.box.$note_box;
|
|
||||||
|
|
||||||
let current_top = Math.round($note_box.position().top);
|
|
||||||
let current_left = Math.round($note_box.position().left);
|
|
||||||
let current_height = $note_box.height();
|
|
||||||
let current_width = $note_box.width();
|
|
||||||
|
|
||||||
switch (event.originalEvent.key) {
|
switch (event.originalEvent.key) {
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
current_height--;
|
this.note.h--;
|
||||||
break;
|
break;
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
current_height++;
|
this.note.h++;
|
||||||
break;
|
break;
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
current_width--;
|
this.note.w--;
|
||||||
break;
|
break;
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
current_width++;
|
this.note.w++;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = this.get_min_max_position(null, null, current_height, current_width);
|
this.place_note(this.note.x, this.note.y, this.note.w, this.note.h);
|
||||||
if (current_top === position.norm_top && current_left === position.norm_left) {
|
|
||||||
$note_box.css({
|
|
||||||
height: current_height,
|
|
||||||
width: current_width,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Note.Body.hide_all();
|
Note.Body.hide_all();
|
||||||
$note_box.addClass("unsaved");
|
this.$note_box.addClass("unsaved");
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,10 +603,10 @@ class Note {
|
|||||||
let text = $dialog.find("textarea").val();
|
let text = $dialog.find("textarea").val();
|
||||||
|
|
||||||
let params = {
|
let params = {
|
||||||
x: Math.round(note.x * note.post_width),
|
x: note.x,
|
||||||
y: Math.round(note.y * note.post_height),
|
y: note.y,
|
||||||
width: Math.round(note.width * note.post_width),
|
width: note.w,
|
||||||
height: Math.round(note.height * note.post_height),
|
height: note.h,
|
||||||
body: text
|
body: text
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -753,13 +739,13 @@ class Note {
|
|||||||
$(document).off("mouseup.danbooru", Note.TranslationMode.Drag.stop);
|
$(document).off("mouseup.danbooru", Note.TranslationMode.Drag.stop);
|
||||||
|
|
||||||
if ($("#note-preview").is(":visible")) {
|
if ($("#note-preview").is(":visible")) {
|
||||||
let $image = $("#image");
|
let scale_factor = $(".note-container").width() / parseInt($(".note-container").attr("data-width"));
|
||||||
|
|
||||||
new Note({
|
new Note({
|
||||||
x: $("#note-preview").position().left / $image.width(),
|
x: $("#note-preview").position().left / scale_factor,
|
||||||
y: $("#note-preview").position().top / $image.height(),
|
y: $("#note-preview").position().top / scale_factor,
|
||||||
w: $("#note-preview").width() / $image.width(),
|
w: $("#note-preview").width() / scale_factor,
|
||||||
h: $("#note-preview").height() / $image.height(),
|
h: $("#note-preview").height() / scale_factor,
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#note-preview").hide();
|
$("#note-preview").hide();
|
||||||
@@ -778,6 +764,10 @@ class Note {
|
|||||||
this.post_id = this.$note_container.data("id");
|
this.post_id = this.$note_container.data("id");
|
||||||
this.embed = Utility.meta("post-has-embedded-notes") === "true";
|
this.embed = Utility.meta("post-has-embedded-notes") === "true";
|
||||||
this.original_body = original_body;
|
this.original_body = original_body;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.w = w;
|
||||||
|
this.h = h;
|
||||||
|
|
||||||
this.box = new Note.Box(this);
|
this.box = new Note.Box(this);
|
||||||
this.body = new Note.Body(this);
|
this.body = new Note.Body(this);
|
||||||
@@ -792,23 +782,9 @@ class Note {
|
|||||||
return this.id === null;
|
return this.id === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The coordinates of the top left corner of the note box, as floats in the
|
// The ratio of the current image size to the full image size.
|
||||||
// range 0.0 to 1.0. Does not account for note rotation.
|
get scale_factor() {
|
||||||
get x() {
|
return this.$note_container.width() / this.post_width;
|
||||||
return this.box.$note_box.position().left / this.image_width;
|
|
||||||
}
|
|
||||||
|
|
||||||
get y() {
|
|
||||||
return this.box.$note_box.position().top / this.image_height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The current width and height of the note box, as floats in the range 0.0 to 1.0.
|
|
||||||
get width() {
|
|
||||||
return this.box.$note_box.width() / this.image_width;
|
|
||||||
}
|
|
||||||
|
|
||||||
get height() {
|
|
||||||
return this.box.$note_box.height() / this.image_height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The width and height of the full-size original image in pixels.
|
// The width and height of the full-size original image in pixels.
|
||||||
@@ -885,17 +861,15 @@ class Note {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static load_all() {
|
static load_all() {
|
||||||
let $image = $(".image-container");
|
|
||||||
|
|
||||||
$("#notes article").toArray().forEach(article => {
|
$("#notes article").toArray().forEach(article => {
|
||||||
var $article = $(article);
|
var $article = $(article);
|
||||||
|
|
||||||
new Note({
|
new Note({
|
||||||
id: $article.data("id"),
|
id: $article.data("id"),
|
||||||
x: $article.data("x") / $image.data("width"),
|
x: $article.data("x"),
|
||||||
y: $article.data("y") / $image.data("height"),
|
y: $article.data("y"),
|
||||||
w: $article.data("width") / $image.data("width"),
|
w: $article.data("width"),
|
||||||
h: $article.data("height") / $image.data("height"),
|
h: $article.data("height"),
|
||||||
original_body: $article.data("body"),
|
original_body: $article.data("body"),
|
||||||
sanitized_body: $article.html()
|
sanitized_body: $article.html()
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user