Skip to content

Commit

Permalink
[Editor] Improve drawing on a touch screen.
Browse files Browse the repository at this point in the history
- it's now possible to start a drawing with a pen and use fingers to zoom
  or scroll without interacting with the current drawing;
- it's now possible to draw with a finger and them zoom with two fingers.
  • Loading branch information
calixteman committed Dec 10, 2024
1 parent 4d22881 commit b301be4
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 45 deletions.
6 changes: 3 additions & 3 deletions src/display/editor/annotation_editor_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,9 @@ class AnnotationEditorLayer {
this.div.addEventListener("pointerdown", this.pointerdown.bind(this), {
signal,
});
this.div.addEventListener("pointerup", this.pointerup.bind(this), {
signal,
});
const pointerup = this.pointerup.bind(this);
this.div.addEventListener("pointerup", pointerup, { signal });
this.div.addEventListener("pointercancel", pointerup, { signal });
}

disableClick() {
Expand Down
194 changes: 153 additions & 41 deletions src/display/editor/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,21 @@ class DrawingEditor extends AnnotationEditor {

static _currentDrawId = -1;

static _currentDraw = null;
static _currentParent = null;

static _currentDrawingOptions = null;
static #currentDraw = null;

static _currentParent = null;
static #currentDrawingAC = null;

static #currentDrawingOptions = null;

static #currentPointerId = 0;

static #currentPointerType = null;

static #currentPointerIds = null;

static #currentMoveTimestamp = 0;

static _INNER_MARGIN = 3;

Expand Down Expand Up @@ -168,7 +178,7 @@ class DrawingEditor extends AnnotationEditor {
this._defaultDrawingOptions.updateProperty(propertyName, value);
}
if (this._currentParent) {
this._currentDraw.updateProperty(propertyName, value);
DrawingEditor.#currentDraw.updateProperty(propertyName, value);
this._currentParent.drawLayer.updateProperties(
this._currentDrawId,
this._defaultDrawingOptions.toSVGProperties()
Expand Down Expand Up @@ -639,32 +649,81 @@ class DrawingEditor extends AnnotationEditor {
unreachable("Not implemented");
}

static startDrawing(
parent,
uiManager,
_isLTR,
{ target, offsetX: x, offsetY: y }
) {
static startDrawing(parent, uiManager, _isLTR, event) {
// The _currentPointerType is set when the user starts an empty drawing
// session. If, in the same drawing session, the user starts using a
// different type of pointer (e.g. a pen and then a finger), we just return.
//
// The _currentPointerId and _currentPointerIds are used to keep track of
// the pointers with a same type (e.g. two fingers). If the user starts to
// draw with a finger and then uses a second finger, we just stop the
// current drawing and let the user zoom the document.

const { target, offsetX: x, offsetY: y, pointerId, pointerType } = event;
if (
DrawingEditor.#currentPointerType &&
DrawingEditor.#currentPointerType !== pointerType
) {
return;
}

const {
viewport: { rotation },
} = parent;
const { width: parentWidth, height: parentHeight } =
target.getBoundingClientRect();
const ac = new AbortController();

const ac = (DrawingEditor.#currentDrawingAC = new AbortController());
const signal = parent.combinedSignal(ac);

DrawingEditor.#currentPointerId ??= pointerId;
DrawingEditor.#currentPointerType ??= pointerType;

window.addEventListener(
"pointerup",
e => {
ac.abort();
parent.toggleDrawing(true);
this._endDraw(e);
if (DrawingEditor.#currentPointerId === e.pointerId) {
this._endDraw(e);
} else {
DrawingEditor.#currentPointerIds?.delete(e.pointerId);
}
},
{ signal }
);
window.addEventListener(
"pointercancel",
e => {
if (DrawingEditor.#currentPointerId === e.pointerId) {
this._currentParent.endDrawingSession();
} else {
DrawingEditor.#currentPointerIds?.delete(e.pointerId);
}
},
{ signal }
);
window.addEventListener(
"pointerdown",
stopEvent /* Avoid to have undesired clicks during drawing. */,
e => {
if (DrawingEditor.#currentPointerType !== e.pointerType) {
// For example, we started with a pen and the user
// is now using a finger.
return;
}

// For example, the user is using a second finger.
(DrawingEditor.#currentPointerIds ||= new Set()).add(e.pointerId);

// The first finger created a first point and a second finger just
// started, so we stop the drawing and remove this only point.
if (DrawingEditor.#currentDraw.isCancellable()) {
DrawingEditor.#currentDraw.removeLastElement();
if (DrawingEditor.#currentDraw.isEmpty()) {
this._currentParent.endDrawingSession(/* isAborted = */ true);
} else {
this._endDraw(null);
}
}
},
{
capture: true,
passive: false,
Expand All @@ -675,54 +734,114 @@ class DrawingEditor extends AnnotationEditor {
target.addEventListener("pointermove", this._drawMove.bind(this), {
signal,
});
target.addEventListener(
"touchmove",
e => {
if (e.timeStamp === DrawingEditor.#currentMoveTimestamp) {
// This move event is used to draw so we don't want to scroll.
stopEvent(e);
}
},
{ signal }
);
parent.toggleDrawing();
uiManager._editorUndoBar?.hide();

if (this._currentDraw) {
if (DrawingEditor.#currentDraw) {
parent.drawLayer.updateProperties(
this._currentDrawId,
this._currentDraw.startNew(x, y, parentWidth, parentHeight, rotation)
DrawingEditor.#currentDraw.startNew(
x,
y,
parentWidth,
parentHeight,
rotation
)
);
return;
}

uiManager.updateUIForDefaultProperties(this);

this._currentDraw = this.createDrawerInstance(
DrawingEditor.#currentDraw = this.createDrawerInstance(
x,
y,
parentWidth,
parentHeight,
rotation
);
this._currentDrawingOptions = this.getDefaultDrawingOptions();
DrawingEditor.#currentDrawingOptions = this.getDefaultDrawingOptions();
this._currentParent = parent;

({ id: this._currentDrawId } = parent.drawLayer.draw(
this._mergeSVGProperties(
this._currentDrawingOptions.toSVGProperties(),
this._currentDraw.defaultSVGProperties
DrawingEditor.#currentDrawingOptions.toSVGProperties(),
DrawingEditor.#currentDraw.defaultSVGProperties
),
/* isPathUpdatable = */ true,
/* hasClip = */ false
));
}

static _drawMove({ offsetX, offsetY }) {
static _drawMove(event) {
DrawingEditor.#currentMoveTimestamp = -1;
if (!DrawingEditor.#currentDraw) {
return;
}
const { offsetX, offsetY, pointerId } = event;

if (DrawingEditor.#currentPointerId !== pointerId) {
return;
}
if (DrawingEditor.#currentPointerIds?.size >= 1) {
// The user is using multiple fingers and the first one is moving.
this._endDraw(event);
return;
}
this._currentParent.drawLayer.updateProperties(
this._currentDrawId,
this._currentDraw.add(offsetX, offsetY)
DrawingEditor.#currentDraw.add(offsetX, offsetY)
);
// We track the timestamp to know if the touchmove event is used to draw.
DrawingEditor.#currentMoveTimestamp = event.timeStamp;
stopEvent(event);
}

static _cleanup(all) {
if (all) {
this._currentDrawId = -1;
this._currentParent = null;
DrawingEditor.#currentDraw = null;
DrawingEditor.#currentDrawingOptions = null;
DrawingEditor.#currentPointerType = null;
DrawingEditor.#currentMoveTimestamp = 0;
}

if (DrawingEditor.#currentDrawingAC) {
DrawingEditor.#currentDrawingAC.abort();
DrawingEditor.#currentDrawingAC = null;
DrawingEditor.#currentPointerId = 0;
DrawingEditor.#currentPointerIds = null;
}
}

static _endDraw({ offsetX, offsetY }) {
static _endDraw(event) {
const parent = this._currentParent;
parent.drawLayer.updateProperties(
this._currentDrawId,
this._currentDraw.end(offsetX, offsetY)
);
if (!parent) {
return;
}

parent.toggleDrawing(true);
this._cleanup(false);

if (event) {
parent.drawLayer.updateProperties(
this._currentDrawId,
DrawingEditor.#currentDraw.end(event.offsetX, event.offsetY)
);
}
if (this.supportMultipleDrawings) {
const draw = this._currentDraw;
const draw = DrawingEditor.#currentDraw;
const drawId = this._currentDrawId;
const lastElement = draw.getLastElement();
parent.addCommands({
Expand Down Expand Up @@ -753,7 +872,7 @@ class DrawingEditor extends AnnotationEditor {
parent.toggleDrawing(true);
parent.cleanUndoStack(AnnotationEditorParamsType.DRAW_STEP);

if (!this._currentDraw.isEmpty()) {
if (!DrawingEditor.#currentDraw.isEmpty()) {
const {
pageDimensions: [pageWidth, pageHeight],
scale,
Expand All @@ -764,32 +883,25 @@ class DrawingEditor extends AnnotationEditor {
false,
{
drawId: this._currentDrawId,
drawOutlines: this._currentDraw.getOutlines(
drawOutlines: DrawingEditor.#currentDraw.getOutlines(
pageWidth * scale,
pageHeight * scale,
scale,
this._INNER_MARGIN
),
drawingOptions: this._currentDrawingOptions,
drawingOptions: DrawingEditor.#currentDrawingOptions,
mustBeCommitted: !isAborted,
}
);
this._cleanup();
this._cleanup(true);
return editor;
}

parent.drawLayer.remove(this._currentDrawId);
this._cleanup();
this._cleanup(true);
return null;
}

static _cleanup() {
this._currentDrawId = -1;
this._currentDraw = null;
this._currentDrawingOptions = null;
this._currentParent = null;
}

/**
* Create the drawing options.
* @param {Object} _data
Expand Down
6 changes: 6 additions & 0 deletions src/display/editor/drawers/inkdraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ class InkDrawOutliner {
return !this.#lines || this.#lines.length === 0;
}

isCancellable() {
// The user a second finger after drawing 5 points: it's small enough
// to not be a real drawing.
return this.#points.length <= 10;
}

add(x, y) {
// The point is in canvas coordinates which means that there is no rotation.
// It's the same as parent coordinates.
Expand Down
1 change: 0 additions & 1 deletion web/annotation_editor_layer_builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@

.annotationEditorLayer.inkEditing {
cursor: var(--editorInk-editing-cursor);
touch-action: none;
}

.annotationEditorLayer .draw {
Expand Down

0 comments on commit b301be4

Please sign in to comment.