Skip to content

Focus point can escape view if moving diagonally #276

@varkor

Description

@varkor

If you move the focus point with the arrow keys, pressing right/down individually will keep the focus point on-screen, but if you press both at once, it can escape the view.

The relevant code is:

quiver/src/ui.mjs

Lines 2156 to 2253 in 1203391

if (this.in_mode(UIMode.Default)) {
// Reveal the focus point if it wasn't already visible.
if (!this.focus_point.class_list.contains("focused")) {
this.focus_point.class_list.remove("revealed", "pending", "active");
this.focus_point.class_list.add("focused");
this.toolbar.update(this);
// We first reposition to the correct location, then add the delta after adding
// the `smooth` class (directly below), so that it animates to the new position.
this.reposition_focus_point(this.focus_position);
delay(() => {
this.focus_point.class_list.add("smooth");
this.reposition_focus_point(this.focus_position.add(position_delta));
});
} else {
this.reposition_focus_point(this.focus_position.add(position_delta));
}
this.update_focus_tooltip();
// Reposition the view if the focus point is not complete in-view.
const offset = this.offset_from_position(this.focus_position);
const width = this.cell_size(this.cell_width, this.focus_position.x);
const height = this.cell_size(this.cell_height, this.focus_position.y);
const view = new Dimensions(
document.body.offsetWidth / 2 ** this.scale,
document.body.offsetHeight / 2 ** this.scale,
).sub(Dimensions.diag(CONSTANTS.VIEW_PADDING * 2));
const pan = Offset.zero();
// We only adjust in the direction of movement, to avoid issues with edge cases,
// e.g. where the height of the screen is too small, which can cause panning
// vertically back and forth with each key press.
if (position_delta.x !== 0) {
// Left.
pan.x += Math.min(offset.x - (this.view.x - view.width / 2), 0);
// Right.
pan.x += Math.max(offset.x + width - (this.view.x + view.width / 2), 0);
}
if (position_delta.y !== 0) {
// Top.
pan.y += Math.min(offset.y - (this.view.y - view.height / 2), 0);
// Bottom.
pan.y += Math.max(offset.y + height - (this.view.y + view.height / 2), 0);
}
const start = performance.now();
const view_origin = new Offset(this.view.x, this.view.y);
// We want to transition the view smoothly. We can animate the offset with CSS, but
// the grid is drawn using a <canvas> and so must be updated manually.
const partial_pan = () => {
requestAnimationFrame(() => {
// The panning animation lasts for 0.1 seconds.
const x
= Math.max(Math.min((performance.now() - start) / (1000 * 0.1), 1), 0);
// The definition of the `ease` transition duration in CSS, which is the
// default transition and the one we use.
const ease = new CubicBezier(
Point.zero(),
new Point(0.25, 0.1),
new Point(0.25, 1.0),
Point.diag(1),
);
// Do a binary search to find the value of `t` corresponding to the x
// co-ordinate `x`. The value of `p.y` thereat is the distance through the
// animation.
let p;
let [min, max] = [ease.point(0), ease.point(1)];
if (x === 0) {
p = min;
} else if (x === 1) {
p = max;
} else if (x > 0 && x < 1) {
const EPSILON = 0.01;
const BAIL_OUT = 128;
let i = 0;
while (true) {
p = ease.point((max.t + min.t) / 2);
if (p.x === x || max.t - min.t <= EPSILON || ++i >= BAIL_OUT) {
break;
}
if (x > p.x) {
min = p;
}
if (x < p.x) {
max = p;
}
}
}
this.pan_to(view_origin.add(pan.mul(p.y)));
if (x < 1) {
partial_pan();
}
})
};
partial_pan();
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugsomething isn't workingui-uxsubpar user interface or user experience

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions