Merge pull request #17506 from calixteman/editor_free_highlight
[Editor] Add the ability to make a free highlight (i.e. without having to select some text) (bug 1856218)
This commit is contained in:
commit
1cdbcfef82
@ -28,6 +28,8 @@ class DrawLayer {
|
||||
|
||||
#mapping = new Map();
|
||||
|
||||
#toUpdate = new Map();
|
||||
|
||||
constructor({ pageIndex }) {
|
||||
this.pageIndex = pageIndex;
|
||||
}
|
||||
@ -53,7 +55,7 @@ class DrawLayer {
|
||||
return shadow(this, "_svgFactory", new DOMSVGFactory());
|
||||
}
|
||||
|
||||
static #setBox(element, { x, y, width, height }) {
|
||||
static #setBox(element, { x = 0, y = 0, width = 1, height = 1 } = {}) {
|
||||
const { style } = element;
|
||||
style.top = `${100 * y}%`;
|
||||
style.left = `${100 * x}%`;
|
||||
@ -83,10 +85,13 @@ class DrawLayer {
|
||||
return clipPathId;
|
||||
}
|
||||
|
||||
highlight(outlines, color, opacity) {
|
||||
highlight(outlines, color, opacity, isPathUpdatable = false) {
|
||||
const id = this.#id++;
|
||||
const root = this.#createSVG(outlines.box);
|
||||
root.classList.add("highlight");
|
||||
if (outlines.free) {
|
||||
root.classList.add("free");
|
||||
}
|
||||
const defs = DrawLayer._svgFactory.createElement("defs");
|
||||
root.append(defs);
|
||||
const path = DrawLayer._svgFactory.createElement("path");
|
||||
@ -95,6 +100,10 @@ class DrawLayer {
|
||||
path.setAttribute("id", pathId);
|
||||
path.setAttribute("d", outlines.toSVGPath());
|
||||
|
||||
if (isPathUpdatable) {
|
||||
this.#toUpdate.set(id, path);
|
||||
}
|
||||
|
||||
// Create the clipping path for the editor div.
|
||||
const clipPathId = this.#createClipPath(defs, pathId);
|
||||
|
||||
@ -139,6 +148,22 @@ class DrawLayer {
|
||||
return id;
|
||||
}
|
||||
|
||||
finalizeLine(id, line) {
|
||||
const path = this.#toUpdate.get(id);
|
||||
this.#toUpdate.delete(id);
|
||||
this.updateBox(id, line.box);
|
||||
path.setAttribute("d", line.toSVGPath());
|
||||
}
|
||||
|
||||
removeFreeHighlight(id) {
|
||||
this.remove(id);
|
||||
this.#toUpdate.delete(id);
|
||||
}
|
||||
|
||||
updatePath(id, line) {
|
||||
this.#toUpdate.get(id).setAttribute("d", line.toSVGPath());
|
||||
}
|
||||
|
||||
updateBox(id, box) {
|
||||
DrawLayer.#setBox(this.#mapping.get(id), box);
|
||||
}
|
||||
|
@ -67,6 +67,8 @@ class AnnotationEditorLayer {
|
||||
|
||||
#boundPointerdown = this.pointerdown.bind(this);
|
||||
|
||||
#boundTextLayerPointerDown = this.#textLayerPointerDown.bind(this);
|
||||
|
||||
#editorFocusTimeoutId = null;
|
||||
|
||||
#boundSelectionStart = this.selectionStart.bind(this);
|
||||
@ -199,7 +201,7 @@ class AnnotationEditorLayer {
|
||||
}
|
||||
}
|
||||
|
||||
const editor = this.#createAndAddNewEditor(
|
||||
const editor = this.createAndAddNewEditor(
|
||||
{ offsetX: 0, offsetY: 0 },
|
||||
/* isCentered = */ false
|
||||
);
|
||||
@ -328,12 +330,34 @@ class AnnotationEditorLayer {
|
||||
enableTextSelection() {
|
||||
if (this.#textLayer?.div) {
|
||||
document.addEventListener("selectstart", this.#boundSelectionStart);
|
||||
this.#textLayer.div.addEventListener(
|
||||
"pointerdown",
|
||||
this.#boundTextLayerPointerDown
|
||||
);
|
||||
this.#textLayer.div.classList.add("drawing");
|
||||
}
|
||||
}
|
||||
|
||||
disableTextSelection() {
|
||||
if (this.#textLayer?.div) {
|
||||
document.removeEventListener("selectstart", this.#boundSelectionStart);
|
||||
this.#textLayer.div.removeEventListener(
|
||||
"pointerdown",
|
||||
this.#boundTextLayerPointerDown
|
||||
);
|
||||
this.#textLayer.div.classList.remove("drawing");
|
||||
}
|
||||
}
|
||||
|
||||
#textLayerPointerDown(event) {
|
||||
if (event.target === this.#textLayer.div) {
|
||||
const { isMac } = FeatureTest.platform;
|
||||
if (event.button !== 0 || (event.ctrlKey && isMac)) {
|
||||
// Do nothing on right click.
|
||||
return;
|
||||
}
|
||||
HighlightEditor.startHighlighting(this, event);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@ -565,7 +589,7 @@ class AnnotationEditorLayer {
|
||||
* @param [Object] data
|
||||
* @returns {AnnotationEditor}
|
||||
*/
|
||||
#createAndAddNewEditor(event, isCentered, data = {}) {
|
||||
createAndAddNewEditor(event, isCentered, data = {}) {
|
||||
const id = this.getNextId();
|
||||
const editor = this.#createNewEditor({
|
||||
parent: this,
|
||||
@ -603,10 +627,7 @@ class AnnotationEditorLayer {
|
||||
* Create and add a new editor.
|
||||
*/
|
||||
addNewEditor() {
|
||||
this.#createAndAddNewEditor(
|
||||
this.#getCenterPoint(),
|
||||
/* isCentered = */ true
|
||||
);
|
||||
this.createAndAddNewEditor(this.#getCenterPoint(), /* isCentered = */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -726,7 +747,7 @@ class AnnotationEditorLayer {
|
||||
boxes.push(rotator(x, y, width, height));
|
||||
}
|
||||
if (boxes.length !== 0) {
|
||||
this.#createAndAddNewEditor(event, false, {
|
||||
this.createAndAddNewEditor(event, false, {
|
||||
boxes,
|
||||
});
|
||||
}
|
||||
@ -767,7 +788,7 @@ class AnnotationEditorLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#createAndAddNewEditor(event, /* isCentered = */ false);
|
||||
this.createAndAddNewEditor(event, /* isCentered = */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -901,6 +922,10 @@ class AnnotationEditorLayer {
|
||||
const { pageWidth, pageHeight } = this.viewport.rawDims;
|
||||
return [pageWidth, pageHeight];
|
||||
}
|
||||
|
||||
get scale() {
|
||||
return this.#uiManager.viewParameters.realScale;
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationEditorLayer };
|
||||
|
@ -18,10 +18,11 @@ import {
|
||||
AnnotationEditorType,
|
||||
Util,
|
||||
} from "../../shared/util.js";
|
||||
import { FreeOutliner, Outliner } from "./outliner.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
import { bindEvents } from "./tools.js";
|
||||
import { ColorPicker } from "./color_picker.js";
|
||||
import { Outliner } from "./outliner.js";
|
||||
import { noContextMenu } from "../display_utils.js";
|
||||
|
||||
/**
|
||||
* Basic draw editor in order to generate an Highlight annotation.
|
||||
@ -41,6 +42,8 @@ class HighlightEditor extends AnnotationEditor {
|
||||
|
||||
#id = null;
|
||||
|
||||
#isFreeHighlight = false;
|
||||
|
||||
#lastPoint = null;
|
||||
|
||||
#opacity;
|
||||
@ -51,12 +54,20 @@ class HighlightEditor extends AnnotationEditor {
|
||||
|
||||
static _defaultOpacity = 1;
|
||||
|
||||
static _defaultThickness = 10;
|
||||
|
||||
static _l10nPromise;
|
||||
|
||||
static _type = "highlight";
|
||||
|
||||
static _editorType = AnnotationEditorType.HIGHLIGHT;
|
||||
|
||||
static _freeHighlightId = -1;
|
||||
|
||||
static _freeHighlight = null;
|
||||
|
||||
static _freeHighlightClipId = "";
|
||||
|
||||
constructor(params) {
|
||||
super({ ...params, name: "highlightEditor" });
|
||||
this.color = params.color || HighlightEditor._defaultColor;
|
||||
@ -64,9 +75,15 @@ class HighlightEditor extends AnnotationEditor {
|
||||
this.#boxes = params.boxes || null;
|
||||
this._isDraggable = false;
|
||||
|
||||
this.#createOutlines();
|
||||
this.#addToDrawLayer();
|
||||
this.rotate(this.rotation);
|
||||
if (params.highlightId > -1) {
|
||||
this.#isFreeHighlight = true;
|
||||
this.#createFreeOutlines(params);
|
||||
this.#addToDrawLayer();
|
||||
} else {
|
||||
this.#createOutlines();
|
||||
this.#addToDrawLayer();
|
||||
this.rotate(this.rotation);
|
||||
}
|
||||
}
|
||||
|
||||
#createOutlines() {
|
||||
@ -95,6 +112,60 @@ class HighlightEditor extends AnnotationEditor {
|
||||
];
|
||||
}
|
||||
|
||||
#createFreeOutlines({ highlight, highlightId, clipPathId }) {
|
||||
this.#highlightOutlines = highlight.getOutlines(
|
||||
this._uiManager.direction === "ltr"
|
||||
);
|
||||
this.#id = highlightId;
|
||||
this.#clipPathId = clipPathId;
|
||||
const { x, y, width, height, lastPoint } = this.#highlightOutlines.box;
|
||||
|
||||
// We need to redraw the highlight because we change the coordinates to be
|
||||
// in the box coordinate system.
|
||||
this.parent.drawLayer.finalizeLine(this.#id, this.#highlightOutlines);
|
||||
switch (this.rotation) {
|
||||
case 0:
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
break;
|
||||
case 90: {
|
||||
const [pageWidth, pageHeight] = this.parentDimensions;
|
||||
this.x = y;
|
||||
this.y = 1 - x;
|
||||
this.width = (width * pageHeight) / pageWidth;
|
||||
this.height = (height * pageWidth) / pageHeight;
|
||||
break;
|
||||
}
|
||||
case 180:
|
||||
this.x = 1 - x;
|
||||
this.y = 1 - y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
break;
|
||||
case 270: {
|
||||
const [pageWidth, pageHeight] = this.parentDimensions;
|
||||
this.x = 1 - y;
|
||||
this.y = x;
|
||||
this.width = (width * pageHeight) / pageWidth;
|
||||
this.height = (height * pageWidth) / pageHeight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const innerMargin = 1.5;
|
||||
this.#focusOutlines = highlight.getFocusOutline(
|
||||
/* Slightly bigger than the highlight in order to have a little
|
||||
space between the highlight and the outline. */
|
||||
HighlightEditor._defaultThickness + innerMargin
|
||||
);
|
||||
this.#outlineId = this.parent.drawLayer.highlightOutline(
|
||||
this.#focusOutlines
|
||||
);
|
||||
this.#lastPoint = lastPoint;
|
||||
}
|
||||
|
||||
static initialize(l10n, uiManager) {
|
||||
AnnotationEditor.initialize(l10n, uiManager);
|
||||
HighlightEditor._defaultColor ||=
|
||||
@ -196,12 +267,12 @@ class HighlightEditor extends AnnotationEditor {
|
||||
|
||||
/** @inheritdoc */
|
||||
fixAndSetPosition() {
|
||||
return super.fixAndSetPosition(0);
|
||||
return super.fixAndSetPosition(this.#getRotation());
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
getRect(tx, ty) {
|
||||
return super.getRect(tx, ty, 0);
|
||||
return super.getRect(tx, ty, this.#getRotation());
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -229,7 +300,7 @@ class HighlightEditor extends AnnotationEditor {
|
||||
this.#addToDrawLayer();
|
||||
|
||||
if (!this.isAttachedToDOM) {
|
||||
// At some point this editor was removed and we're rebuilting it,
|
||||
// At some point this editor was removed and we're rebuilding it,
|
||||
// hence we must add it to its parent.
|
||||
this.parent.add(this);
|
||||
}
|
||||
@ -273,10 +344,10 @@ class HighlightEditor extends AnnotationEditor {
|
||||
this.color,
|
||||
this.#opacity
|
||||
));
|
||||
this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
|
||||
if (this.#highlightDiv) {
|
||||
this.#highlightDiv.style.clipPath = this.#clipPathId;
|
||||
}
|
||||
this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
|
||||
}
|
||||
|
||||
static #rotateBbox({ x, y, width, height }, angle) {
|
||||
@ -313,10 +384,19 @@ class HighlightEditor extends AnnotationEditor {
|
||||
|
||||
/** @inheritdoc */
|
||||
rotate(angle) {
|
||||
// We need to rotate the svgs because of the coordinates system.
|
||||
const { drawLayer } = this.parent;
|
||||
let box;
|
||||
if (this.#isFreeHighlight) {
|
||||
angle = (angle - this.rotation + 360) % 360;
|
||||
box = HighlightEditor.#rotateBbox(this.#highlightOutlines.box, angle);
|
||||
} else {
|
||||
// An highlight annotation is always drawn horizontally.
|
||||
box = HighlightEditor.#rotateBbox(this, angle);
|
||||
}
|
||||
drawLayer.rotate(this.#id, angle);
|
||||
drawLayer.rotate(this.#outlineId, angle);
|
||||
drawLayer.updateBox(this.#id, HighlightEditor.#rotateBbox(this, angle));
|
||||
drawLayer.updateBox(this.#id, box);
|
||||
drawLayer.updateBox(
|
||||
this.#outlineId,
|
||||
HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
|
||||
@ -330,6 +410,9 @@ class HighlightEditor extends AnnotationEditor {
|
||||
}
|
||||
|
||||
const div = super.render();
|
||||
if (this.#isFreeHighlight) {
|
||||
div.classList.add("free");
|
||||
}
|
||||
const highlightDiv = (this.#highlightDiv = document.createElement("div"));
|
||||
div.append(highlightDiv);
|
||||
highlightDiv.className = "internal";
|
||||
@ -364,7 +447,16 @@ class HighlightEditor extends AnnotationEditor {
|
||||
this.parent?.drawLayer.removeClass(this.#outlineId, "selected");
|
||||
}
|
||||
|
||||
#getRotation() {
|
||||
// Highlight annotations are always drawn horizontally but if
|
||||
// a free highlight annotation can be rotated.
|
||||
return this.#isFreeHighlight ? this.rotation : 0;
|
||||
}
|
||||
|
||||
#serializeBoxes(rect) {
|
||||
if (this.#isFreeHighlight) {
|
||||
return null;
|
||||
}
|
||||
const [pageWidth, pageHeight] = this.pageDimensions;
|
||||
const boxes = this.#boxes;
|
||||
const quadPoints = new Array(boxes.length * 8);
|
||||
@ -387,7 +479,79 @@ class HighlightEditor extends AnnotationEditor {
|
||||
}
|
||||
|
||||
#serializeOutlines(rect) {
|
||||
return this.#highlightOutlines.serialize(rect, 0);
|
||||
return this.#highlightOutlines.serialize(rect, this.#getRotation());
|
||||
}
|
||||
|
||||
static startHighlighting(parent, { target: textLayer, x, y }) {
|
||||
const {
|
||||
x: layerX,
|
||||
y: layerY,
|
||||
width: parentWidth,
|
||||
height: parentHeight,
|
||||
} = textLayer.getBoundingClientRect();
|
||||
const pointerMove = e => {
|
||||
this.#highlightMove(parent, e);
|
||||
};
|
||||
const pointerDownOptions = { capture: true, passive: false };
|
||||
const pointerDown = e => {
|
||||
// Avoid to have undesired clicks during the drawing.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const pointerUpCallback = e => {
|
||||
textLayer.removeEventListener("pointermove", pointerMove);
|
||||
window.removeEventListener("blur", pointerUpCallback);
|
||||
window.removeEventListener("pointerup", pointerUpCallback);
|
||||
window.removeEventListener(
|
||||
"pointerdown",
|
||||
pointerDown,
|
||||
pointerDownOptions
|
||||
);
|
||||
window.removeEventListener("contextmenu", noContextMenu);
|
||||
this.#endHighlight(parent, e);
|
||||
};
|
||||
window.addEventListener("blur", pointerUpCallback);
|
||||
window.addEventListener("pointerup", pointerUpCallback);
|
||||
window.addEventListener("pointerdown", pointerDown, pointerDownOptions);
|
||||
window.addEventListener("contextmenu", noContextMenu);
|
||||
|
||||
textLayer.addEventListener("pointermove", pointerMove);
|
||||
this._freeHighlight = new FreeOutliner(
|
||||
{ x, y },
|
||||
[layerX, layerY, parentWidth, parentHeight],
|
||||
parent.scale,
|
||||
this._defaultThickness,
|
||||
/* innerMargin = */ 0.001
|
||||
);
|
||||
({ id: this._freeHighlightId, clipPathId: this._freeHighlightClipId } =
|
||||
parent.drawLayer.highlight(
|
||||
this._freeHighlight,
|
||||
this._defaultColor,
|
||||
this._defaultOpacity,
|
||||
/* isPathUpdatable = */ true
|
||||
));
|
||||
}
|
||||
|
||||
static #highlightMove(parent, event) {
|
||||
if (this._freeHighlight.add(event)) {
|
||||
// Redraw only if the point has been added.
|
||||
parent.drawLayer.updatePath(this._freeHighlightId, this._freeHighlight);
|
||||
}
|
||||
}
|
||||
|
||||
static #endHighlight(parent, event) {
|
||||
if (!this._freeHighlight.isEmpty()) {
|
||||
parent.createAndAddNewEditor(event, false, {
|
||||
highlightId: this._freeHighlightId,
|
||||
highlight: this._freeHighlight,
|
||||
clipPathId: this._freeHighlightClipId,
|
||||
});
|
||||
} else {
|
||||
parent.drawLayer.removeFreeHighlight(this._freeHighlightId);
|
||||
}
|
||||
this._freeHighlightId = -1;
|
||||
this._freeHighlight = null;
|
||||
this._freeHighlightClipId = "";
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -437,7 +601,7 @@ class HighlightEditor extends AnnotationEditor {
|
||||
outlines: this.#serializeOutlines(rect),
|
||||
pageIndex: this.pageIndex,
|
||||
rect,
|
||||
rotation: 0,
|
||||
rotation: this.#getRotation(),
|
||||
structTreeParentId: this._structTreeParentId,
|
||||
};
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Util } from "../../shared/util.js";
|
||||
|
||||
class Outliner {
|
||||
#box;
|
||||
|
||||
@ -260,10 +262,16 @@ class Outliner {
|
||||
}
|
||||
|
||||
class Outline {
|
||||
/**
|
||||
* @returns {string} The SVG path of the outline.
|
||||
*/
|
||||
toSVGPath() {
|
||||
throw new Error("Abstract method `toSVGPath` must be implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Object|null} The bounding box of the outline.
|
||||
*/
|
||||
get box() {
|
||||
throw new Error("Abstract getter `box` must be implemented.");
|
||||
}
|
||||
@ -271,6 +279,10 @@ class Outline {
|
||||
serialize(_bbox, _rotation) {
|
||||
throw new Error("Abstract method `serialize` must be implemented.");
|
||||
}
|
||||
|
||||
get free() {
|
||||
return this instanceof FreeHighlightOutline;
|
||||
}
|
||||
}
|
||||
|
||||
class HighlightOutline extends Outline {
|
||||
@ -331,4 +343,469 @@ class HighlightOutline extends Outline {
|
||||
}
|
||||
}
|
||||
|
||||
export { Outliner };
|
||||
class FreeOutliner {
|
||||
#box;
|
||||
|
||||
#bottom = [];
|
||||
|
||||
#innerMargin;
|
||||
|
||||
#top = [];
|
||||
|
||||
// The first 6 elements are the last 3 points of the top part of the outline.
|
||||
// The next 6 elements are the last 3 points of the line.
|
||||
// The next 6 elements are the last 3 points of the bottom part of the
|
||||
// outline.
|
||||
// We track the last 3 points in order to be able to:
|
||||
// - compute the normal of the line,
|
||||
// - compute the control points of the quadratic Bézier curve.
|
||||
#last = new Float64Array(18);
|
||||
|
||||
#min;
|
||||
|
||||
#min_dist;
|
||||
|
||||
#scaleFactor;
|
||||
|
||||
#thickness;
|
||||
|
||||
#points = [];
|
||||
|
||||
static #MIN_DIST = 8;
|
||||
|
||||
static #MIN_DIFF = 2;
|
||||
|
||||
static #MIN = FreeOutliner.#MIN_DIST + FreeOutliner.#MIN_DIFF;
|
||||
|
||||
constructor({ x, y }, box, scaleFactor, thickness, innerMargin = 0) {
|
||||
this.#box = box;
|
||||
this.#thickness = thickness * scaleFactor;
|
||||
this.#last.set([NaN, NaN, NaN, NaN, x, y], 6);
|
||||
this.#innerMargin = innerMargin;
|
||||
this.#min_dist = FreeOutliner.#MIN_DIST * scaleFactor;
|
||||
this.#min = FreeOutliner.#MIN * scaleFactor;
|
||||
this.#scaleFactor = scaleFactor;
|
||||
this.#points.push(x, y);
|
||||
}
|
||||
|
||||
get free() {
|
||||
return true;
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
// When we add a second point then this.#last.slice(6) will be something
|
||||
// like [NaN, NaN, firstX, firstY, secondX, secondY,...] so having a NaN
|
||||
// at index 8 means that we've only one point.
|
||||
return isNaN(this.#last[8]);
|
||||
}
|
||||
|
||||
add({ x, y }) {
|
||||
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
|
||||
let [x1, y1, x2, y2] = this.#last.subarray(8, 12);
|
||||
const diffX = x - x2;
|
||||
const diffY = y - y2;
|
||||
const d = Math.hypot(diffX, diffY);
|
||||
if (d < this.#min) {
|
||||
// The idea is to avoid garbage points around the last point.
|
||||
// When the points are too close, it just leads to bad normal vectors and
|
||||
// control points.
|
||||
return false;
|
||||
}
|
||||
const diffD = d - this.#min_dist;
|
||||
const K = diffD / d;
|
||||
const shiftX = K * diffX;
|
||||
const shiftY = K * diffY;
|
||||
|
||||
// We update the last 3 points of the line.
|
||||
let x0 = x1;
|
||||
let y0 = y1;
|
||||
x1 = x2;
|
||||
y1 = y2;
|
||||
x2 += shiftX;
|
||||
y2 += shiftY;
|
||||
|
||||
// We keep track of the points in order to be able to compute the focus
|
||||
// outline.
|
||||
this.#points?.push(x, y);
|
||||
|
||||
// Create the normal unit vector.
|
||||
// |(shiftX, shiftY)| = |K| * |(diffX, diffY)| = |K| * d = diffD.
|
||||
const nX = -shiftY / diffD;
|
||||
const nY = shiftX / diffD;
|
||||
const thX = nX * this.#thickness;
|
||||
const thY = nY * this.#thickness;
|
||||
this.#last.set(this.#last.subarray(2, 8), 0);
|
||||
this.#last.set([x2 + thX, y2 + thY], 4);
|
||||
this.#last.set(this.#last.subarray(14, 18), 12);
|
||||
this.#last.set([x2 - thX, y2 - thY], 16);
|
||||
|
||||
if (isNaN(this.#last[6])) {
|
||||
if (this.#top.length === 0) {
|
||||
this.#last.set([x1 + thX, y1 + thY], 2);
|
||||
this.#top.push(
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
(x1 + thX - layerX) / layerWidth,
|
||||
(y1 + thY - layerY) / layerHeight
|
||||
);
|
||||
this.#last.set([x1 - thX, y1 - thY], 14);
|
||||
this.#bottom.push(
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
(x1 - thX - layerX) / layerWidth,
|
||||
(y1 - thY - layerY) / layerHeight
|
||||
);
|
||||
}
|
||||
this.#last.set([x0, y0, x1, y1, x2, y2], 6);
|
||||
return !this.isEmpty();
|
||||
}
|
||||
|
||||
this.#last.set([x0, y0, x1, y1, x2, y2], 6);
|
||||
|
||||
const angle = Math.abs(
|
||||
Math.atan2(y0 - y1, x0 - x1) - Math.atan2(shiftY, shiftX)
|
||||
);
|
||||
if (angle < Math.PI / 2) {
|
||||
// In order to avoid some possible artifacts, we're going to use the a
|
||||
// straight line instead of a quadratic Bézier curve.
|
||||
[x1, y1, x2, y2] = this.#last.subarray(2, 6);
|
||||
this.#top.push(
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
((x1 + x2) / 2 - layerX) / layerWidth,
|
||||
((y1 + y2) / 2 - layerY) / layerHeight
|
||||
);
|
||||
[x1, y1, x0, y0] = this.#last.subarray(14, 18);
|
||||
this.#bottom.push(
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
((x0 + x1) / 2 - layerX) / layerWidth,
|
||||
((y0 + y1) / 2 - layerY) / layerHeight
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Control points and the final point for the quadratic Bézier curve.
|
||||
[x0, y0, x1, y1, x2, y2] = this.#last.subarray(0, 6);
|
||||
this.#top.push(
|
||||
((x0 + 5 * x1) / 6 - layerX) / layerWidth,
|
||||
((y0 + 5 * y1) / 6 - layerY) / layerHeight,
|
||||
((5 * x1 + x2) / 6 - layerX) / layerWidth,
|
||||
((5 * y1 + y2) / 6 - layerY) / layerHeight,
|
||||
((x1 + x2) / 2 - layerX) / layerWidth,
|
||||
((y1 + y2) / 2 - layerY) / layerHeight
|
||||
);
|
||||
[x2, y2, x1, y1, x0, y0] = this.#last.subarray(12, 18);
|
||||
this.#bottom.push(
|
||||
((x0 + 5 * x1) / 6 - layerX) / layerWidth,
|
||||
((y0 + 5 * y1) / 6 - layerY) / layerHeight,
|
||||
((5 * x1 + x2) / 6 - layerX) / layerWidth,
|
||||
((5 * y1 + y2) / 6 - layerY) / layerHeight,
|
||||
((x1 + x2) / 2 - layerX) / layerWidth,
|
||||
((y1 + y2) / 2 - layerY) / layerHeight
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
toSVGPath() {
|
||||
if (this.isEmpty()) {
|
||||
// We've only one point.
|
||||
return "";
|
||||
}
|
||||
const top = this.#top;
|
||||
const bottom = this.#bottom;
|
||||
const lastTop = this.#last.subarray(4, 6);
|
||||
const lastBottom = this.#last.subarray(16, 18);
|
||||
const [x, y, width, height] = this.#box;
|
||||
|
||||
if (isNaN(this.#last[6]) && !this.isEmpty()) {
|
||||
// We've only two points.
|
||||
return `M${(this.#last[2] - x) / width} ${
|
||||
(this.#last[3] - y) / height
|
||||
} L${(this.#last[4] - x) / width} ${(this.#last[5] - y) / height} L${
|
||||
(this.#last[16] - x) / width
|
||||
} ${(this.#last[17] - y) / height} L${(this.#last[14] - x) / width} ${
|
||||
(this.#last[15] - y) / height
|
||||
} Z`;
|
||||
}
|
||||
|
||||
const buffer = [];
|
||||
buffer.push(`M${top[4]} ${top[5]}`);
|
||||
for (let i = 6; i < top.length; i += 6) {
|
||||
if (isNaN(top[i])) {
|
||||
buffer.push(`L${top[i + 4]} ${top[i + 5]}`);
|
||||
} else {
|
||||
buffer.push(
|
||||
`C${top[i]} ${top[i + 1]} ${top[i + 2]} ${top[i + 3]} ${top[i + 4]} ${
|
||||
top[i + 5]
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
buffer.push(
|
||||
`L${(lastTop[0] - x) / width} ${(lastTop[1] - y) / height} L${
|
||||
(lastBottom[0] - x) / width
|
||||
} ${(lastBottom[1] - y) / height}`
|
||||
);
|
||||
for (let i = bottom.length - 6; i >= 6; i -= 6) {
|
||||
if (isNaN(bottom[i])) {
|
||||
buffer.push(`L${bottom[i + 4]} ${bottom[i + 5]}`);
|
||||
} else {
|
||||
buffer.push(
|
||||
`C${bottom[i]} ${bottom[i + 1]} ${bottom[i + 2]} ${bottom[i + 3]} ${
|
||||
bottom[i + 4]
|
||||
} ${bottom[i + 5]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
buffer.push(`L${bottom[4]} ${bottom[5]} Z`);
|
||||
|
||||
return buffer.join(" ");
|
||||
}
|
||||
|
||||
getFocusOutline(thickness) {
|
||||
// Build the outline of the highlight to use as the focus outline.
|
||||
const [x, y] = this.#points;
|
||||
const outliner = new FreeOutliner(
|
||||
{ x, y },
|
||||
this.#box,
|
||||
this.#scaleFactor,
|
||||
thickness,
|
||||
/* innerMargin = */ 0.0025
|
||||
);
|
||||
outliner.#points = null;
|
||||
for (let i = 2; i < this.#points.length; i += 2) {
|
||||
outliner.add({ x: this.#points[i], y: this.#points[i + 1] });
|
||||
}
|
||||
return outliner.getOutlines();
|
||||
}
|
||||
|
||||
getOutlines(isLTR) {
|
||||
const top = this.#top;
|
||||
const bottom = this.#bottom;
|
||||
const last = this.#last;
|
||||
const lastTop = last.subarray(4, 6);
|
||||
const lastBottom = last.subarray(16, 18);
|
||||
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
|
||||
|
||||
if (isNaN(last[6]) && !this.isEmpty()) {
|
||||
// We've only two points.
|
||||
const outline = new Float64Array(24);
|
||||
outline.set(
|
||||
[
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
(last[2] - layerX) / layerWidth,
|
||||
(last[3] - layerY) / layerHeight,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
(last[4] - layerX) / layerWidth,
|
||||
(last[5] - layerY) / layerHeight,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
(last[16] - layerX) / layerWidth,
|
||||
(last[17] - layerY) / layerHeight,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
(last[14] - layerX) / layerWidth,
|
||||
(last[15] - layerY) / layerHeight,
|
||||
],
|
||||
0
|
||||
);
|
||||
return new FreeHighlightOutline(outline, this.#innerMargin, isLTR);
|
||||
}
|
||||
|
||||
const outline = new Float64Array(
|
||||
this.#top.length + 12 + this.#bottom.length
|
||||
);
|
||||
let N = top.length;
|
||||
for (let i = 0; i < N; i += 2) {
|
||||
if (isNaN(top[i])) {
|
||||
outline[i] = outline[i + 1] = NaN;
|
||||
continue;
|
||||
}
|
||||
outline[i] = top[i];
|
||||
outline[i + 1] = top[i + 1];
|
||||
}
|
||||
outline.set(
|
||||
[
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
(lastTop[0] - layerX) / layerWidth,
|
||||
(lastTop[1] - layerY) / layerHeight,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
(lastBottom[0] - layerX) / layerWidth,
|
||||
(lastBottom[1] - layerY) / layerHeight,
|
||||
],
|
||||
N
|
||||
);
|
||||
N += 12;
|
||||
|
||||
for (let i = bottom.length - 6; i >= 6; i -= 6) {
|
||||
for (let j = 0; j < 6; j += 2) {
|
||||
if (isNaN(bottom[i + j])) {
|
||||
outline[N] = outline[N + 1] = NaN;
|
||||
N += 2;
|
||||
continue;
|
||||
}
|
||||
outline[N] = bottom[i + j];
|
||||
outline[N + 1] = bottom[i + j + 1];
|
||||
N += 2;
|
||||
}
|
||||
}
|
||||
outline.set([NaN, NaN, NaN, NaN, bottom[4], bottom[5]], N);
|
||||
return new FreeHighlightOutline(outline, this.#innerMargin, isLTR);
|
||||
}
|
||||
}
|
||||
|
||||
class FreeHighlightOutline extends Outline {
|
||||
#bbox = null;
|
||||
|
||||
#innerMargin;
|
||||
|
||||
#outline;
|
||||
|
||||
constructor(outline, innerMargin, isLTR) {
|
||||
super();
|
||||
this.#outline = outline;
|
||||
this.#innerMargin = innerMargin;
|
||||
this.#computeMinMax(isLTR);
|
||||
|
||||
const { x, y, width, height } = this.#bbox;
|
||||
for (let i = 0, ii = outline.length; i < ii; i += 2) {
|
||||
outline[i] = (outline[i] - x) / width;
|
||||
outline[i + 1] = (outline[i + 1] - y) / height;
|
||||
}
|
||||
}
|
||||
|
||||
toSVGPath() {
|
||||
const buffer = [`M${this.#outline[4]} ${this.#outline[5]}`];
|
||||
for (let i = 6, ii = this.#outline.length; i < ii; i += 6) {
|
||||
if (isNaN(this.#outline[i])) {
|
||||
buffer.push(`L${this.#outline[i + 4]} ${this.#outline[i + 5]}`);
|
||||
continue;
|
||||
}
|
||||
buffer.push(
|
||||
`C${this.#outline[i]} ${this.#outline[i + 1]} ${this.#outline[i + 2]} ${
|
||||
this.#outline[i + 3]
|
||||
} ${this.#outline[i + 4]} ${this.#outline[i + 5]}`
|
||||
);
|
||||
}
|
||||
buffer.push("Z");
|
||||
return buffer.join(" ");
|
||||
}
|
||||
|
||||
serialize([blX, blY, trX, trY], rotation) {
|
||||
const src = this.#outline;
|
||||
const outline = new Float64Array(src.length);
|
||||
const width = trX - blX;
|
||||
const height = trY - blY;
|
||||
switch (rotation) {
|
||||
case 0:
|
||||
for (let i = 0, ii = src.length; i < ii; i += 2) {
|
||||
outline[i] = blX + src[i] * width;
|
||||
outline[i + 1] = trY - src[i + 1] * height;
|
||||
}
|
||||
break;
|
||||
case 90:
|
||||
for (let i = 0, ii = src.length; i < ii; i += 2) {
|
||||
outline[i] = blX + src[i + 1] * width;
|
||||
outline[i + 1] = blY + src[i] * height;
|
||||
}
|
||||
break;
|
||||
case 180:
|
||||
for (let i = 0, ii = src.length; i < ii; i += 2) {
|
||||
outline[i] = trX - src[i] * width;
|
||||
outline[i + 1] = blY + src[i + 1] * height;
|
||||
}
|
||||
break;
|
||||
case 270:
|
||||
for (let i = 0, ii = src.length; i < ii; i += 2) {
|
||||
outline[i] = trX - src[i + 1] * width;
|
||||
outline[i + 1] = trY - src[i] * height;
|
||||
}
|
||||
}
|
||||
return outline;
|
||||
}
|
||||
|
||||
#computeMinMax(isLTR) {
|
||||
const outline = this.#outline;
|
||||
let lastX = outline[4];
|
||||
let lastY = outline[5];
|
||||
let minX = lastX;
|
||||
let minY = lastY;
|
||||
let maxX = lastX;
|
||||
let maxY = lastY;
|
||||
let lastPointX = lastX;
|
||||
let lastPointY = lastY;
|
||||
const ltrCallback = isLTR ? Math.max : Math.min;
|
||||
|
||||
for (let i = 6, ii = outline.length; i < ii; i += 6) {
|
||||
if (isNaN(outline[i])) {
|
||||
minX = Math.min(minX, outline[i + 4]);
|
||||
minY = Math.min(minY, outline[i + 5]);
|
||||
maxX = Math.max(maxX, outline[i + 4]);
|
||||
maxY = Math.max(maxY, outline[i + 5]);
|
||||
if (lastPointY < outline[i + 5]) {
|
||||
lastPointX = outline[i + 4];
|
||||
lastPointY = outline[i + 5];
|
||||
} else if (lastPointY === outline[i + 5]) {
|
||||
lastPointX = ltrCallback(lastPointX, outline[i + 4]);
|
||||
}
|
||||
} else {
|
||||
const bbox = Util.bezierBoundingBox(
|
||||
lastX,
|
||||
lastY,
|
||||
...outline.slice(i, i + 6)
|
||||
);
|
||||
minX = Math.min(minX, bbox[0]);
|
||||
minY = Math.min(minY, bbox[1]);
|
||||
maxX = Math.max(maxX, bbox[2]);
|
||||
maxY = Math.max(maxY, bbox[3]);
|
||||
if (lastPointY < bbox[3]) {
|
||||
lastPointX = bbox[2];
|
||||
lastPointY = bbox[3];
|
||||
} else if (lastPointY === bbox[3]) {
|
||||
lastPointX = ltrCallback(lastPointX, bbox[2]);
|
||||
}
|
||||
}
|
||||
lastX = outline[i + 4];
|
||||
lastY = outline[i + 5];
|
||||
}
|
||||
|
||||
const x = minX - this.#innerMargin,
|
||||
y = minY - this.#innerMargin,
|
||||
width = maxX - minX + 2 * this.#innerMargin,
|
||||
height = maxY - minY + 2 * this.#innerMargin;
|
||||
lastPointX = (lastPointX - x) / width;
|
||||
lastPointY = (lastPointY - y) / height;
|
||||
this.#bbox = { x, y, width, height, lastPoint: [lastPointX, lastPointY] };
|
||||
}
|
||||
|
||||
get box() {
|
||||
return this.#bbox;
|
||||
}
|
||||
}
|
||||
|
||||
export { FreeOutliner, Outliner };
|
||||
|
@ -942,25 +942,25 @@
|
||||
|
||||
.annotationEditorLayer {
|
||||
&[data-main-rotation="0"] {
|
||||
.highlightEditor > .editToolbar {
|
||||
.highlightEditor:not(.free) > .editToolbar {
|
||||
rotate: 0deg;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-main-rotation="90"] {
|
||||
.highlightEditor > .editToolbar {
|
||||
.highlightEditor:not(.free) > .editToolbar {
|
||||
rotate: 270deg;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-main-rotation="180"] {
|
||||
.highlightEditor > .editToolbar {
|
||||
.highlightEditor:not(.free) > .editToolbar {
|
||||
rotate: 180deg;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-main-rotation="270"] {
|
||||
.highlightEditor > .editToolbar {
|
||||
.highlightEditor:not(.free) > .editToolbar {
|
||||
rotate: 90deg;
|
||||
}
|
||||
}
|
||||
@ -969,14 +969,17 @@
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
transform-origin: 0 0;
|
||||
cursor: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
pointer-events: none;
|
||||
transform: none;
|
||||
transform-origin: 0 0;
|
||||
|
||||
&:not(.free) {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.internal {
|
||||
position: absolute;
|
||||
|
@ -44,7 +44,10 @@
|
||||
|
||||
position: absolute;
|
||||
mix-blend-mode: var(--blend-mode);
|
||||
fill-rule: evenodd;
|
||||
|
||||
&:not(.free) {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
}
|
||||
|
||||
&.highlightOutline {
|
||||
|
@ -25,6 +25,10 @@
|
||||
transform-origin: 0 0;
|
||||
z-index: 2;
|
||||
|
||||
&.drawing {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
:is(span, br) {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
|
Loading…
Reference in New Issue
Block a user