2022-06-04 23:28:19 +02:00
|
|
|
/* Copyright 2022 Mozilla Foundation
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-06-13 18:23:10 +02:00
|
|
|
import {
|
|
|
|
AnnotationEditorParamsType,
|
|
|
|
AnnotationEditorType,
|
|
|
|
Util,
|
|
|
|
} from "../../shared/util.js";
|
2022-06-04 23:28:19 +02:00
|
|
|
import { AnnotationEditor } from "./editor.js";
|
2023-06-05 11:32:44 +02:00
|
|
|
import { InkAnnotationElement } from "../annotation_layer.js";
|
2022-07-24 22:19:09 +02:00
|
|
|
import { opacityToHex } from "./tools.js";
|
2022-06-04 23:28:19 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Basic draw editor in order to generate an Ink annotation.
|
|
|
|
*/
|
|
|
|
class InkEditor extends AnnotationEditor {
|
2022-06-16 18:16:49 +02:00
|
|
|
#baseHeight = 0;
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2022-06-16 18:16:49 +02:00
|
|
|
#baseWidth = 0;
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
#boundCanvasContextMenu = this.canvasContextMenu.bind(this);
|
|
|
|
|
2022-07-19 18:20:56 +02:00
|
|
|
#boundCanvasPointermove = this.canvasPointermove.bind(this);
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2022-07-19 18:20:56 +02:00
|
|
|
#boundCanvasPointerleave = this.canvasPointerleave.bind(this);
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2022-07-19 18:20:56 +02:00
|
|
|
#boundCanvasPointerup = this.canvasPointerup.bind(this);
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2022-07-19 18:20:56 +02:00
|
|
|
#boundCanvasPointerdown = this.canvasPointerdown.bind(this);
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
#currentPath2D = new Path2D();
|
|
|
|
|
2022-06-16 18:16:49 +02:00
|
|
|
#disableEditing = false;
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
#hasSomethingToDraw = false;
|
2022-07-12 17:32:14 +02:00
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
#isCanvasInitialized = false;
|
2022-07-24 22:19:09 +02:00
|
|
|
|
2022-06-16 18:16:49 +02:00
|
|
|
#observer = null;
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2022-06-23 15:47:45 +02:00
|
|
|
#realWidth = 0;
|
|
|
|
|
|
|
|
#realHeight = 0;
|
|
|
|
|
2022-07-24 22:19:09 +02:00
|
|
|
#requestFrameCallback = null;
|
|
|
|
|
2022-06-29 15:39:02 +02:00
|
|
|
static _defaultColor = null;
|
2022-06-13 18:23:10 +02:00
|
|
|
|
2022-07-24 22:19:09 +02:00
|
|
|
static _defaultOpacity = 1;
|
|
|
|
|
2022-06-29 15:39:02 +02:00
|
|
|
static _defaultThickness = 1;
|
2022-06-13 18:23:10 +02:00
|
|
|
|
2022-06-28 18:21:32 +02:00
|
|
|
static _l10nPromise;
|
|
|
|
|
2022-08-02 18:29:01 +02:00
|
|
|
static _type = "ink";
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
constructor(params) {
|
|
|
|
super({ ...params, name: "inkEditor" });
|
2022-07-12 17:32:14 +02:00
|
|
|
this.color = params.color || null;
|
|
|
|
this.thickness = params.thickness || null;
|
2022-07-24 22:19:09 +02:00
|
|
|
this.opacity = params.opacity || null;
|
2022-06-04 23:28:19 +02:00
|
|
|
this.paths = [];
|
|
|
|
this.bezierPath2D = [];
|
2023-02-09 11:16:10 +01:00
|
|
|
this.allRawPaths = [];
|
2022-06-04 23:28:19 +02:00
|
|
|
this.currentPath = [];
|
|
|
|
this.scaleFactor = 1;
|
|
|
|
this.translationX = this.translationY = 0;
|
|
|
|
this.x = 0;
|
|
|
|
this.y = 0;
|
|
|
|
}
|
|
|
|
|
2023-06-22 12:16:07 +02:00
|
|
|
/** @inheritdoc */
|
2022-06-28 18:21:32 +02:00
|
|
|
static initialize(l10n) {
|
|
|
|
this._l10nPromise = new Map(
|
2022-09-01 16:15:27 +02:00
|
|
|
["editor_ink_canvas_aria_label", "editor_ink2_aria_label"].map(str => [
|
2022-06-28 18:21:32 +02:00
|
|
|
str,
|
|
|
|
l10n.get(str),
|
|
|
|
])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-06-22 12:16:07 +02:00
|
|
|
/** @inheritdoc */
|
2022-06-13 18:23:10 +02:00
|
|
|
static updateDefaultParams(type, value) {
|
|
|
|
switch (type) {
|
|
|
|
case AnnotationEditorParamsType.INK_THICKNESS:
|
|
|
|
InkEditor._defaultThickness = value;
|
|
|
|
break;
|
|
|
|
case AnnotationEditorParamsType.INK_COLOR:
|
|
|
|
InkEditor._defaultColor = value;
|
|
|
|
break;
|
2022-07-24 22:19:09 +02:00
|
|
|
case AnnotationEditorParamsType.INK_OPACITY:
|
|
|
|
InkEditor._defaultOpacity = value / 100;
|
|
|
|
break;
|
2022-06-13 18:23:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
|
|
|
updateParams(type, value) {
|
|
|
|
switch (type) {
|
|
|
|
case AnnotationEditorParamsType.INK_THICKNESS:
|
|
|
|
this.#updateThickness(value);
|
|
|
|
break;
|
|
|
|
case AnnotationEditorParamsType.INK_COLOR:
|
|
|
|
this.#updateColor(value);
|
|
|
|
break;
|
2022-07-24 22:19:09 +02:00
|
|
|
case AnnotationEditorParamsType.INK_OPACITY:
|
|
|
|
this.#updateOpacity(value);
|
|
|
|
break;
|
2022-06-13 18:23:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-22 12:16:07 +02:00
|
|
|
/** @inheritdoc */
|
2022-06-13 18:23:10 +02:00
|
|
|
static get defaultPropertiesToUpdate() {
|
|
|
|
return [
|
|
|
|
[AnnotationEditorParamsType.INK_THICKNESS, InkEditor._defaultThickness],
|
2022-06-29 15:39:02 +02:00
|
|
|
[
|
|
|
|
AnnotationEditorParamsType.INK_COLOR,
|
|
|
|
InkEditor._defaultColor || AnnotationEditor._defaultLineColor,
|
|
|
|
],
|
2022-07-24 22:19:09 +02:00
|
|
|
[
|
|
|
|
AnnotationEditorParamsType.INK_OPACITY,
|
|
|
|
Math.round(InkEditor._defaultOpacity * 100),
|
|
|
|
],
|
2022-06-13 18:23:10 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
|
|
|
get propertiesToUpdate() {
|
|
|
|
return [
|
2022-07-21 10:42:15 +02:00
|
|
|
[
|
|
|
|
AnnotationEditorParamsType.INK_THICKNESS,
|
|
|
|
this.thickness || InkEditor._defaultThickness,
|
|
|
|
],
|
|
|
|
[
|
|
|
|
AnnotationEditorParamsType.INK_COLOR,
|
|
|
|
this.color ||
|
|
|
|
InkEditor._defaultColor ||
|
|
|
|
AnnotationEditor._defaultLineColor,
|
|
|
|
],
|
2022-07-24 22:19:09 +02:00
|
|
|
[
|
|
|
|
AnnotationEditorParamsType.INK_OPACITY,
|
|
|
|
Math.round(100 * (this.opacity ?? InkEditor._defaultOpacity)),
|
|
|
|
],
|
2022-06-13 18:23:10 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the thickness and make this action undoable.
|
|
|
|
* @param {number} thickness
|
|
|
|
*/
|
|
|
|
#updateThickness(thickness) {
|
|
|
|
const savedThickness = this.thickness;
|
2022-12-05 12:25:06 +01:00
|
|
|
this.addCommands({
|
2022-06-13 18:23:10 +02:00
|
|
|
cmd: () => {
|
|
|
|
this.thickness = thickness;
|
2022-07-29 16:53:03 +02:00
|
|
|
this.#fitToContent();
|
2022-06-13 18:23:10 +02:00
|
|
|
},
|
|
|
|
undo: () => {
|
|
|
|
this.thickness = savedThickness;
|
2022-07-29 16:53:03 +02:00
|
|
|
this.#fitToContent();
|
2022-06-13 18:23:10 +02:00
|
|
|
},
|
|
|
|
mustExec: true,
|
|
|
|
type: AnnotationEditorParamsType.INK_THICKNESS,
|
|
|
|
overwriteIfSameType: true,
|
|
|
|
keepUndo: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the color and make this action undoable.
|
|
|
|
* @param {string} color
|
|
|
|
*/
|
|
|
|
#updateColor(color) {
|
|
|
|
const savedColor = this.color;
|
2022-12-05 12:25:06 +01:00
|
|
|
this.addCommands({
|
2022-06-13 18:23:10 +02:00
|
|
|
cmd: () => {
|
|
|
|
this.color = color;
|
|
|
|
this.#redraw();
|
|
|
|
},
|
|
|
|
undo: () => {
|
|
|
|
this.color = savedColor;
|
|
|
|
this.#redraw();
|
|
|
|
},
|
|
|
|
mustExec: true,
|
|
|
|
type: AnnotationEditorParamsType.INK_COLOR,
|
|
|
|
overwriteIfSameType: true,
|
|
|
|
keepUndo: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-24 22:19:09 +02:00
|
|
|
/**
|
|
|
|
* Update the opacity and make this action undoable.
|
|
|
|
* @param {number} opacity
|
|
|
|
*/
|
|
|
|
#updateOpacity(opacity) {
|
|
|
|
opacity /= 100;
|
|
|
|
const savedOpacity = this.opacity;
|
2022-12-05 12:25:06 +01:00
|
|
|
this.addCommands({
|
2022-07-24 22:19:09 +02:00
|
|
|
cmd: () => {
|
|
|
|
this.opacity = opacity;
|
|
|
|
this.#redraw();
|
|
|
|
},
|
|
|
|
undo: () => {
|
|
|
|
this.opacity = savedOpacity;
|
|
|
|
this.#redraw();
|
|
|
|
},
|
|
|
|
mustExec: true,
|
|
|
|
type: AnnotationEditorParamsType.INK_OPACITY,
|
|
|
|
overwriteIfSameType: true,
|
|
|
|
keepUndo: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
/** @inheritdoc */
|
|
|
|
rebuild() {
|
2022-07-21 10:42:15 +02:00
|
|
|
super.rebuild();
|
2022-06-04 23:28:19 +02:00
|
|
|
if (this.div === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.canvas) {
|
|
|
|
this.#createCanvas();
|
|
|
|
this.#createObserver();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.isAttachedToDOM) {
|
|
|
|
// At some point this editor was removed and we're rebuilding it,
|
|
|
|
// hence we must add it to its parent.
|
|
|
|
this.parent.add(this);
|
|
|
|
this.#setCanvasDims();
|
|
|
|
}
|
|
|
|
this.#fitToContent();
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
|
|
|
remove() {
|
|
|
|
if (this.canvas === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-07-04 18:04:32 +02:00
|
|
|
if (!this.isEmpty()) {
|
|
|
|
this.commit();
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
// Destroy the canvas.
|
2022-07-04 18:04:32 +02:00
|
|
|
this.canvas.width = this.canvas.height = 0;
|
2022-06-04 23:28:19 +02:00
|
|
|
this.canvas.remove();
|
|
|
|
this.canvas = null;
|
|
|
|
|
|
|
|
this.#observer.disconnect();
|
|
|
|
this.#observer = null;
|
2022-06-16 18:16:49 +02:00
|
|
|
|
|
|
|
super.remove();
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
2022-12-05 12:25:06 +01:00
|
|
|
setParent(parent) {
|
|
|
|
if (!this.parent && parent) {
|
|
|
|
// We've a parent hence the rescale will be handled thanks to the
|
|
|
|
// ResizeObserver.
|
|
|
|
this._uiManager.removeShouldRescale(this);
|
|
|
|
} else if (this.parent && parent === null) {
|
|
|
|
// The editor is removed from the DOM, hence we handle the rescale thanks
|
|
|
|
// to the onScaleChanging callback.
|
|
|
|
// This way, it'll be saved/printed correctly.
|
|
|
|
this._uiManager.addShouldRescale(this);
|
|
|
|
}
|
|
|
|
super.setParent(parent);
|
|
|
|
}
|
|
|
|
|
|
|
|
onScaleChanging() {
|
|
|
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
|
|
|
const width = this.width * parentWidth;
|
|
|
|
const height = this.height * parentHeight;
|
|
|
|
this.setDimensions(width, height);
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
/** @inheritdoc */
|
|
|
|
enableEditMode() {
|
2022-06-16 18:16:49 +02:00
|
|
|
if (this.#disableEditing || this.canvas === null) {
|
2022-06-04 23:28:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
super.enableEditMode();
|
|
|
|
this.div.draggable = false;
|
2022-07-19 18:20:56 +02:00
|
|
|
this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown);
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
|
|
|
disableEditMode() {
|
2022-06-23 15:47:45 +02:00
|
|
|
if (!this.isInEditMode() || this.canvas === null) {
|
2022-06-04 23:28:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
super.disableEditMode();
|
2022-06-16 18:16:49 +02:00
|
|
|
this.div.draggable = !this.isEmpty();
|
2022-06-04 23:28:19 +02:00
|
|
|
this.div.classList.remove("editing");
|
|
|
|
|
2022-07-19 18:20:56 +02:00
|
|
|
this.canvas.removeEventListener(
|
|
|
|
"pointerdown",
|
|
|
|
this.#boundCanvasPointerdown
|
|
|
|
);
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
|
|
|
onceAdded() {
|
2022-06-16 18:16:49 +02:00
|
|
|
this.div.draggable = !this.isEmpty();
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
|
|
|
isEmpty() {
|
2022-07-04 18:04:32 +02:00
|
|
|
return (
|
|
|
|
this.paths.length === 0 ||
|
|
|
|
(this.paths.length === 1 && this.paths[0].length === 0)
|
|
|
|
);
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
2022-06-23 15:47:45 +02:00
|
|
|
#getInitialBBox() {
|
2022-12-05 12:25:06 +01:00
|
|
|
const {
|
|
|
|
parentRotation,
|
|
|
|
parentDimensions: [width, height],
|
|
|
|
} = this;
|
|
|
|
switch (parentRotation) {
|
2022-06-23 15:47:45 +02:00
|
|
|
case 90:
|
2022-12-05 12:25:06 +01:00
|
|
|
return [0, height, height, width];
|
2022-06-23 15:47:45 +02:00
|
|
|
case 180:
|
|
|
|
return [width, height, width, height];
|
|
|
|
case 270:
|
2022-12-05 12:25:06 +01:00
|
|
|
return [width, 0, height, width];
|
2022-06-23 15:47:45 +02:00
|
|
|
default:
|
|
|
|
return [0, 0, width, height];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
/**
|
|
|
|
* Set line styles.
|
|
|
|
*/
|
|
|
|
#setStroke() {
|
2022-12-05 12:25:06 +01:00
|
|
|
const { ctx, color, opacity, thickness, parentScale, scaleFactor } = this;
|
|
|
|
ctx.lineWidth = (thickness * parentScale) / scaleFactor;
|
|
|
|
ctx.lineCap = "round";
|
|
|
|
ctx.lineJoin = "round";
|
|
|
|
ctx.miterLimit = 10;
|
|
|
|
ctx.strokeStyle = `${color}${opacityToHex(opacity)}`;
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start to draw on the canvas.
|
|
|
|
* @param {number} x
|
|
|
|
* @param {number} y
|
|
|
|
*/
|
|
|
|
#startDrawing(x, y) {
|
2023-02-09 11:16:10 +01:00
|
|
|
this.canvas.addEventListener("contextmenu", this.#boundCanvasContextMenu);
|
|
|
|
this.canvas.addEventListener("pointerleave", this.#boundCanvasPointerleave);
|
|
|
|
this.canvas.addEventListener("pointermove", this.#boundCanvasPointermove);
|
|
|
|
this.canvas.addEventListener("pointerup", this.#boundCanvasPointerup);
|
|
|
|
this.canvas.removeEventListener(
|
|
|
|
"pointerdown",
|
|
|
|
this.#boundCanvasPointerdown
|
|
|
|
);
|
|
|
|
|
2022-07-21 10:42:15 +02:00
|
|
|
this.isEditing = true;
|
2022-07-12 17:32:14 +02:00
|
|
|
if (!this.#isCanvasInitialized) {
|
|
|
|
this.#isCanvasInitialized = true;
|
|
|
|
this.#setCanvasDims();
|
|
|
|
this.thickness ||= InkEditor._defaultThickness;
|
|
|
|
this.color ||=
|
|
|
|
InkEditor._defaultColor || AnnotationEditor._defaultLineColor;
|
2022-07-24 22:19:09 +02:00
|
|
|
this.opacity ??= InkEditor._defaultOpacity;
|
2022-07-12 17:32:14 +02:00
|
|
|
}
|
2022-06-04 23:28:19 +02:00
|
|
|
this.currentPath.push([x, y]);
|
2023-02-09 11:16:10 +01:00
|
|
|
this.#hasSomethingToDraw = false;
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#setStroke();
|
2022-07-24 22:19:09 +02:00
|
|
|
|
|
|
|
this.#requestFrameCallback = () => {
|
2023-02-09 11:16:10 +01:00
|
|
|
this.#drawPoints();
|
|
|
|
if (this.#requestFrameCallback) {
|
|
|
|
window.requestAnimationFrame(this.#requestFrameCallback);
|
2022-07-24 22:19:09 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
window.requestAnimationFrame(this.#requestFrameCallback);
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draw on the canvas.
|
|
|
|
* @param {number} x
|
|
|
|
* @param {number} y
|
|
|
|
*/
|
|
|
|
#draw(x, y) {
|
2022-07-24 22:19:09 +02:00
|
|
|
const [lastX, lastY] = this.currentPath.at(-1);
|
2023-02-09 11:16:10 +01:00
|
|
|
if (this.currentPath.length > 1 && x === lastX && y === lastY) {
|
2022-07-24 22:19:09 +02:00
|
|
|
return;
|
|
|
|
}
|
2023-02-09 11:16:10 +01:00
|
|
|
const currentPath = this.currentPath;
|
|
|
|
let path2D = this.#currentPath2D;
|
|
|
|
currentPath.push([x, y]);
|
|
|
|
this.#hasSomethingToDraw = true;
|
|
|
|
|
|
|
|
if (currentPath.length <= 2) {
|
|
|
|
path2D.moveTo(...currentPath[0]);
|
|
|
|
path2D.lineTo(x, y);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (currentPath.length === 3) {
|
|
|
|
this.#currentPath2D = path2D = new Path2D();
|
|
|
|
path2D.moveTo(...currentPath[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#makeBezierCurve(
|
|
|
|
path2D,
|
|
|
|
...currentPath.at(-3),
|
|
|
|
...currentPath.at(-2),
|
|
|
|
x,
|
|
|
|
y
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#endPath() {
|
|
|
|
if (this.currentPath.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const lastPoint = this.currentPath.at(-1);
|
|
|
|
this.#currentPath2D.lineTo(...lastPoint);
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop to draw on the canvas.
|
|
|
|
* @param {number} x
|
|
|
|
* @param {number} y
|
|
|
|
*/
|
|
|
|
#stopDrawing(x, y) {
|
2022-07-24 22:19:09 +02:00
|
|
|
this.#requestFrameCallback = null;
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
x = Math.min(Math.max(x, 0), this.canvas.width);
|
|
|
|
y = Math.min(Math.max(y, 0), this.canvas.height);
|
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
this.#draw(x, y);
|
|
|
|
this.#endPath();
|
2022-06-04 23:28:19 +02:00
|
|
|
|
|
|
|
// Interpolate the path entered by the user with some
|
|
|
|
// Bezier's curves in order to have a smoother path and
|
|
|
|
// to reduce the data size used to draw it in the PDF.
|
|
|
|
let bezier;
|
2022-07-24 22:19:09 +02:00
|
|
|
if (this.currentPath.length !== 1) {
|
2023-02-09 11:16:10 +01:00
|
|
|
bezier = this.#generateBezierPoints();
|
2022-06-04 23:28:19 +02:00
|
|
|
} else {
|
|
|
|
// We have only one point finally.
|
|
|
|
const xy = [x, y];
|
|
|
|
bezier = [[xy, xy.slice(), xy.slice(), xy]];
|
|
|
|
}
|
2023-02-09 11:16:10 +01:00
|
|
|
const path2D = this.#currentPath2D;
|
|
|
|
const currentPath = this.currentPath;
|
|
|
|
this.currentPath = [];
|
|
|
|
this.#currentPath2D = new Path2D();
|
2022-06-04 23:28:19 +02:00
|
|
|
|
|
|
|
const cmd = () => {
|
2023-02-09 11:16:10 +01:00
|
|
|
this.allRawPaths.push(currentPath);
|
2022-06-04 23:28:19 +02:00
|
|
|
this.paths.push(bezier);
|
|
|
|
this.bezierPath2D.push(path2D);
|
|
|
|
this.rebuild();
|
|
|
|
};
|
|
|
|
|
|
|
|
const undo = () => {
|
2023-02-09 11:16:10 +01:00
|
|
|
this.allRawPaths.pop();
|
2022-06-04 23:28:19 +02:00
|
|
|
this.paths.pop();
|
|
|
|
this.bezierPath2D.pop();
|
|
|
|
if (this.paths.length === 0) {
|
|
|
|
this.remove();
|
|
|
|
} else {
|
2022-06-16 18:16:49 +02:00
|
|
|
if (!this.canvas) {
|
|
|
|
this.#createCanvas();
|
|
|
|
this.#createObserver();
|
|
|
|
}
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#fitToContent();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-12-05 12:25:06 +01:00
|
|
|
this.addCommands({ cmd, undo, mustExec: true });
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
#drawPoints() {
|
|
|
|
if (!this.#hasSomethingToDraw) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.#hasSomethingToDraw = false;
|
|
|
|
|
|
|
|
const thickness = Math.ceil(this.thickness * this.parentScale);
|
|
|
|
const lastPoints = this.currentPath.slice(-3);
|
|
|
|
const x = lastPoints.map(xy => xy[0]);
|
|
|
|
const y = lastPoints.map(xy => xy[1]);
|
|
|
|
const xMin = Math.min(...x) - thickness;
|
|
|
|
const xMax = Math.max(...x) + thickness;
|
|
|
|
const yMin = Math.min(...y) - thickness;
|
|
|
|
const yMax = Math.max(...y) + thickness;
|
|
|
|
|
|
|
|
const { ctx } = this;
|
|
|
|
ctx.save();
|
|
|
|
|
|
|
|
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
|
|
|
// In Chrome, the clip() method doesn't work as expected.
|
|
|
|
ctx.clearRect(xMin, yMin, xMax - xMin, yMax - yMin);
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.rect(xMin, yMin, xMax - xMin, yMax - yMin);
|
|
|
|
ctx.clip();
|
|
|
|
} else {
|
|
|
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const path of this.bezierPath2D) {
|
|
|
|
ctx.stroke(path);
|
|
|
|
}
|
|
|
|
ctx.stroke(this.#currentPath2D);
|
|
|
|
|
|
|
|
ctx.restore();
|
|
|
|
}
|
|
|
|
|
|
|
|
#makeBezierCurve(path2D, x0, y0, x1, y1, x2, y2) {
|
|
|
|
const prevX = (x0 + x1) / 2;
|
|
|
|
const prevY = (y0 + y1) / 2;
|
|
|
|
const x3 = (x1 + x2) / 2;
|
|
|
|
const y3 = (y1 + y2) / 2;
|
|
|
|
|
|
|
|
path2D.bezierCurveTo(
|
|
|
|
prevX + (2 * (x1 - prevX)) / 3,
|
|
|
|
prevY + (2 * (y1 - prevY)) / 3,
|
|
|
|
x3 + (2 * (x1 - x3)) / 3,
|
|
|
|
y3 + (2 * (y1 - y3)) / 3,
|
|
|
|
x3,
|
|
|
|
y3
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#generateBezierPoints() {
|
|
|
|
const path = this.currentPath;
|
|
|
|
if (path.length <= 2) {
|
|
|
|
return [[path[0], path[0], path.at(-1), path.at(-1)]];
|
|
|
|
}
|
|
|
|
|
|
|
|
const bezierPoints = [];
|
|
|
|
let i;
|
|
|
|
let [x0, y0] = path[0];
|
|
|
|
for (i = 1; i < path.length - 2; i++) {
|
|
|
|
const [x1, y1] = path[i];
|
|
|
|
const [x2, y2] = path[i + 1];
|
|
|
|
const x3 = (x1 + x2) / 2;
|
|
|
|
const y3 = (y1 + y2) / 2;
|
|
|
|
|
|
|
|
// The quadratic is: [[x0, y0], [x1, y1], [x3, y3]].
|
|
|
|
// Convert the quadratic to a cubic
|
|
|
|
// (see https://fontforge.org/docs/techref/bezier.html#converting-truetype-to-postscript)
|
|
|
|
const control1 = [x0 + (2 * (x1 - x0)) / 3, y0 + (2 * (y1 - y0)) / 3];
|
|
|
|
const control2 = [x3 + (2 * (x1 - x3)) / 3, y3 + (2 * (y1 - y3)) / 3];
|
|
|
|
|
|
|
|
bezierPoints.push([[x0, y0], control1, control2, [x3, y3]]);
|
|
|
|
|
|
|
|
[x0, y0] = [x3, y3];
|
|
|
|
}
|
|
|
|
|
|
|
|
const [x1, y1] = path[i];
|
|
|
|
const [x2, y2] = path[i + 1];
|
|
|
|
|
|
|
|
// The quadratic is: [[x0, y0], [x1, y1], [x2, y2]].
|
|
|
|
const control1 = [x0 + (2 * (x1 - x0)) / 3, y0 + (2 * (y1 - y0)) / 3];
|
|
|
|
const control2 = [x2 + (2 * (x1 - x2)) / 3, y2 + (2 * (y1 - y2)) / 3];
|
|
|
|
|
|
|
|
bezierPoints.push([[x0, y0], control1, control2, [x2, y2]]);
|
|
|
|
return bezierPoints;
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
/**
|
|
|
|
* Redraw all the paths.
|
|
|
|
*/
|
|
|
|
#redraw() {
|
|
|
|
if (this.isEmpty()) {
|
|
|
|
this.#updateTransform();
|
|
|
|
return;
|
|
|
|
}
|
2022-07-24 22:19:09 +02:00
|
|
|
this.#setStroke();
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2022-07-24 22:19:09 +02:00
|
|
|
const { canvas, ctx } = this;
|
2022-06-04 23:28:19 +02:00
|
|
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
2022-07-24 22:19:09 +02:00
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#updateTransform();
|
2023-02-09 11:16:10 +01:00
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
for (const path of this.bezierPath2D) {
|
|
|
|
ctx.stroke(path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Commit the curves we have in this editor.
|
|
|
|
*/
|
|
|
|
commit() {
|
|
|
|
if (this.#disableEditing) {
|
|
|
|
return;
|
|
|
|
}
|
2022-06-16 18:16:49 +02:00
|
|
|
|
2022-07-29 18:00:52 +02:00
|
|
|
super.commit();
|
|
|
|
|
2022-07-21 10:42:15 +02:00
|
|
|
this.isEditing = false;
|
2022-06-04 23:28:19 +02:00
|
|
|
this.disableEditMode();
|
|
|
|
|
2022-06-16 18:16:49 +02:00
|
|
|
// This editor must be on top of the main ink editor.
|
|
|
|
this.setInForeground();
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#disableEditing = true;
|
|
|
|
this.div.classList.add("disabled");
|
|
|
|
|
2022-07-29 16:53:03 +02:00
|
|
|
this.#fitToContent(/* firstTime = */ true);
|
2022-07-12 17:32:14 +02:00
|
|
|
|
|
|
|
this.parent.addInkEditorIfNeeded(/* isCommitting = */ true);
|
2022-06-28 18:21:32 +02:00
|
|
|
|
|
|
|
// When commiting, the position of this editor is changed, hence we must
|
|
|
|
// move it to the right position in the DOM.
|
2022-07-28 17:59:03 +02:00
|
|
|
this.parent.moveEditorInDOM(this);
|
2022-12-17 13:48:19 +01:00
|
|
|
this.div.focus({
|
|
|
|
preventScroll: true /* See issue #15744 */,
|
|
|
|
});
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
2022-07-21 10:42:15 +02:00
|
|
|
focusin(event) {
|
|
|
|
super.focusin(event);
|
2022-06-04 23:28:19 +02:00
|
|
|
this.enableEditMode();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-07-19 18:20:56 +02:00
|
|
|
* onpointerdown callback for the canvas we're drawing on.
|
|
|
|
* @param {PointerEvent} event
|
2022-06-04 23:28:19 +02:00
|
|
|
*/
|
2022-07-19 18:20:56 +02:00
|
|
|
canvasPointerdown(event) {
|
2022-07-04 18:04:32 +02:00
|
|
|
if (event.button !== 0 || !this.isInEditMode() || this.#disableEditing) {
|
2022-06-04 23:28:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-06-16 18:16:49 +02:00
|
|
|
// We want to draw on top of any other editors.
|
|
|
|
// Since it's the last child, there's no need to give it a higher z-index.
|
|
|
|
this.setInForeground();
|
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
event.preventDefault();
|
|
|
|
|
2022-07-19 18:20:56 +02:00
|
|
|
if (event.type !== "mouse") {
|
|
|
|
this.div.focus();
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#startDrawing(event.offsetX, event.offsetY);
|
|
|
|
}
|
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
/**
|
|
|
|
* oncontextmenu callback for the canvas we're drawing on.
|
|
|
|
* @param {PointerEvent} event
|
|
|
|
*/
|
|
|
|
canvasContextMenu(event) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
/**
|
2022-07-19 18:20:56 +02:00
|
|
|
* onpointermove callback for the canvas we're drawing on.
|
|
|
|
* @param {PointerEvent} event
|
2022-06-04 23:28:19 +02:00
|
|
|
*/
|
2022-07-19 18:20:56 +02:00
|
|
|
canvasPointermove(event) {
|
2023-02-09 11:16:10 +01:00
|
|
|
event.preventDefault();
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#draw(event.offsetX, event.offsetY);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-07-19 18:20:56 +02:00
|
|
|
* onpointerup callback for the canvas we're drawing on.
|
|
|
|
* @param {PointerEvent} event
|
2022-06-04 23:28:19 +02:00
|
|
|
*/
|
2022-07-19 18:20:56 +02:00
|
|
|
canvasPointerup(event) {
|
2023-02-09 11:16:10 +01:00
|
|
|
event.preventDefault();
|
|
|
|
this.#endDrawing(event);
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-07-19 18:20:56 +02:00
|
|
|
* onpointerleave callback for the canvas we're drawing on.
|
|
|
|
* @param {PointerEvent} event
|
2022-06-04 23:28:19 +02:00
|
|
|
*/
|
2022-07-19 18:20:56 +02:00
|
|
|
canvasPointerleave(event) {
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#endDrawing(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* End the drawing.
|
2022-07-19 18:20:56 +02:00
|
|
|
* @param {PointerEvent} event
|
2022-06-04 23:28:19 +02:00
|
|
|
*/
|
|
|
|
#endDrawing(event) {
|
2022-07-19 18:20:56 +02:00
|
|
|
this.canvas.removeEventListener(
|
|
|
|
"pointerleave",
|
|
|
|
this.#boundCanvasPointerleave
|
|
|
|
);
|
|
|
|
this.canvas.removeEventListener(
|
|
|
|
"pointermove",
|
|
|
|
this.#boundCanvasPointermove
|
|
|
|
);
|
2023-02-09 11:16:10 +01:00
|
|
|
this.canvas.removeEventListener("pointerup", this.#boundCanvasPointerup);
|
|
|
|
this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown);
|
|
|
|
|
|
|
|
// Slight delay to avoid the context menu to appear (it can happen on a long
|
|
|
|
// tap with a pen).
|
|
|
|
setTimeout(() => {
|
|
|
|
this.canvas.removeEventListener(
|
|
|
|
"contextmenu",
|
|
|
|
this.#boundCanvasContextMenu
|
|
|
|
);
|
|
|
|
}, 10);
|
|
|
|
|
|
|
|
this.#stopDrawing(event.offsetX, event.offsetY);
|
2022-07-29 18:00:52 +02:00
|
|
|
|
2022-12-05 12:25:06 +01:00
|
|
|
this.addToAnnotationStorage();
|
2023-02-09 11:16:10 +01:00
|
|
|
|
|
|
|
// Since the ink editor covers all of the page and we want to be able
|
|
|
|
// to select another editor, we just put this one in the background.
|
|
|
|
this.setInBackground();
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the canvas element.
|
|
|
|
*/
|
|
|
|
#createCanvas() {
|
|
|
|
this.canvas = document.createElement("canvas");
|
2022-07-12 17:32:14 +02:00
|
|
|
this.canvas.width = this.canvas.height = 0;
|
2022-06-04 23:28:19 +02:00
|
|
|
this.canvas.className = "inkEditorCanvas";
|
2022-06-28 18:21:32 +02:00
|
|
|
|
|
|
|
InkEditor._l10nPromise
|
|
|
|
.get("editor_ink_canvas_aria_label")
|
|
|
|
.then(msg => this.canvas?.setAttribute("aria-label", msg));
|
2022-06-12 12:20:25 +02:00
|
|
|
this.div.append(this.canvas);
|
2022-06-04 23:28:19 +02:00
|
|
|
this.ctx = this.canvas.getContext("2d");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the resize observer.
|
|
|
|
*/
|
|
|
|
#createObserver() {
|
|
|
|
this.#observer = new ResizeObserver(entries => {
|
|
|
|
const rect = entries[0].contentRect;
|
|
|
|
if (rect.width && rect.height) {
|
|
|
|
this.setDimensions(rect.width, rect.height);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.#observer.observe(this.div);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
|
|
|
render() {
|
|
|
|
if (this.div) {
|
|
|
|
return this.div;
|
|
|
|
}
|
|
|
|
|
2022-06-23 15:47:45 +02:00
|
|
|
let baseX, baseY;
|
|
|
|
if (this.width) {
|
|
|
|
baseX = this.x;
|
|
|
|
baseY = this.y;
|
|
|
|
}
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2022-06-23 15:47:45 +02:00
|
|
|
super.render();
|
2022-06-28 18:21:32 +02:00
|
|
|
|
|
|
|
InkEditor._l10nPromise
|
2022-09-01 16:15:27 +02:00
|
|
|
.get("editor_ink2_aria_label")
|
2022-06-28 18:21:32 +02:00
|
|
|
.then(msg => this.div?.setAttribute("aria-label", msg));
|
|
|
|
|
2022-06-23 15:47:45 +02:00
|
|
|
const [x, y, w, h] = this.#getInitialBBox();
|
|
|
|
this.setAt(x, y, 0, 0);
|
|
|
|
this.setDims(w, h);
|
|
|
|
|
|
|
|
this.#createCanvas();
|
2022-06-04 23:28:19 +02:00
|
|
|
|
|
|
|
if (this.width) {
|
|
|
|
// This editor was created in using copy (ctrl+c).
|
2022-12-05 12:25:06 +01:00
|
|
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
2023-07-06 09:21:34 +02:00
|
|
|
this.setAspectRatio(this.width * parentWidth, this.height * parentHeight);
|
2022-06-23 15:47:45 +02:00
|
|
|
this.setAt(
|
|
|
|
baseX * parentWidth,
|
|
|
|
baseY * parentHeight,
|
|
|
|
this.width * parentWidth,
|
|
|
|
this.height * parentHeight
|
|
|
|
);
|
2022-07-18 14:47:09 +02:00
|
|
|
this.#isCanvasInitialized = true;
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#setCanvasDims();
|
2022-07-18 14:47:09 +02:00
|
|
|
this.setDims(this.width * parentWidth, this.height * parentHeight);
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#redraw();
|
|
|
|
this.div.classList.add("disabled");
|
2022-07-12 17:32:14 +02:00
|
|
|
} else {
|
|
|
|
this.div.classList.add("editing");
|
|
|
|
this.enableEditMode();
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
this.#createObserver();
|
|
|
|
|
|
|
|
return this.div;
|
|
|
|
}
|
|
|
|
|
|
|
|
#setCanvasDims() {
|
2022-07-12 17:32:14 +02:00
|
|
|
if (!this.#isCanvasInitialized) {
|
|
|
|
return;
|
|
|
|
}
|
2022-12-05 12:25:06 +01:00
|
|
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
2022-07-18 14:47:09 +02:00
|
|
|
this.canvas.width = Math.ceil(this.width * parentWidth);
|
|
|
|
this.canvas.height = Math.ceil(this.height * parentHeight);
|
2022-06-04 23:28:19 +02:00
|
|
|
this.#updateTransform();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When the dimensions of the div change the inner canvas must
|
|
|
|
* renew its dimensions, hence it must redraw its own contents.
|
|
|
|
* @param {number} width - the new width of the div
|
|
|
|
* @param {number} height - the new height of the div
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
setDimensions(width, height) {
|
2022-06-23 15:47:45 +02:00
|
|
|
const roundedWidth = Math.round(width);
|
|
|
|
const roundedHeight = Math.round(height);
|
|
|
|
if (
|
|
|
|
this.#realWidth === roundedWidth &&
|
|
|
|
this.#realHeight === roundedHeight
|
|
|
|
) {
|
2022-06-04 23:28:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-06-23 15:47:45 +02:00
|
|
|
this.#realWidth = roundedWidth;
|
|
|
|
this.#realHeight = roundedHeight;
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
this.canvas.style.visibility = "hidden";
|
|
|
|
|
2022-12-05 12:25:06 +01:00
|
|
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
2022-06-23 15:47:45 +02:00
|
|
|
this.width = width / parentWidth;
|
|
|
|
this.height = height / parentHeight;
|
2022-06-04 23:28:19 +02:00
|
|
|
|
|
|
|
if (this.#disableEditing) {
|
2022-07-18 14:47:09 +02:00
|
|
|
this.#setScaleFactor(width, height);
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
this.#setCanvasDims();
|
|
|
|
this.#redraw();
|
|
|
|
|
|
|
|
this.canvas.style.visibility = "visible";
|
2023-03-08 12:10:22 +01:00
|
|
|
|
|
|
|
// For any reason the dimensions couldn't be in percent but in pixels, hence
|
|
|
|
// we must fix them.
|
|
|
|
this.fixDims();
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
2022-07-18 14:47:09 +02:00
|
|
|
#setScaleFactor(width, height) {
|
|
|
|
const padding = this.#getPadding();
|
|
|
|
const scaleFactorW = (width - padding) / this.#baseWidth;
|
|
|
|
const scaleFactorH = (height - padding) / this.#baseHeight;
|
|
|
|
this.scaleFactor = Math.min(scaleFactorW, scaleFactorH);
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
/**
|
|
|
|
* Update the canvas transform.
|
|
|
|
*/
|
|
|
|
#updateTransform() {
|
|
|
|
const padding = this.#getPadding() / 2;
|
|
|
|
this.ctx.setTransform(
|
|
|
|
this.scaleFactor,
|
|
|
|
0,
|
|
|
|
0,
|
|
|
|
this.scaleFactor,
|
|
|
|
this.translationX * this.scaleFactor + padding,
|
|
|
|
this.translationY * this.scaleFactor + padding
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-02-09 11:16:10 +01:00
|
|
|
* Convert into a Path2D.
|
2022-06-04 23:28:19 +02:00
|
|
|
* @param {Arra<Array<number>} bezier
|
|
|
|
* @returns {Path2D}
|
|
|
|
*/
|
2022-07-18 14:47:09 +02:00
|
|
|
static #buildPath2D(bezier) {
|
2022-06-04 23:28:19 +02:00
|
|
|
const path2D = new Path2D();
|
|
|
|
for (let i = 0, ii = bezier.length; i < ii; i++) {
|
|
|
|
const [first, control1, control2, second] = bezier[i];
|
|
|
|
if (i === 0) {
|
|
|
|
path2D.moveTo(...first);
|
|
|
|
}
|
|
|
|
path2D.bezierCurveTo(
|
|
|
|
control1[0],
|
|
|
|
control1[1],
|
|
|
|
control2[0],
|
|
|
|
control2[1],
|
|
|
|
second[0],
|
|
|
|
second[1]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return path2D;
|
|
|
|
}
|
|
|
|
|
2023-05-30 18:24:49 +02:00
|
|
|
static #toPDFCoordinates(points, rect, rotation) {
|
|
|
|
const [blX, blY, trX, trY] = rect;
|
|
|
|
|
|
|
|
switch (rotation) {
|
|
|
|
case 0:
|
|
|
|
for (let i = 0, ii = points.length; i < ii; i += 2) {
|
|
|
|
points[i] += blX;
|
|
|
|
points[i + 1] = trY - points[i + 1];
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 90:
|
|
|
|
for (let i = 0, ii = points.length; i < ii; i += 2) {
|
|
|
|
const x = points[i];
|
|
|
|
points[i] = points[i + 1] + blX;
|
|
|
|
points[i + 1] = x + blY;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 180:
|
|
|
|
for (let i = 0, ii = points.length; i < ii; i += 2) {
|
|
|
|
points[i] = trX - points[i];
|
|
|
|
points[i + 1] += blY;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 270:
|
|
|
|
for (let i = 0, ii = points.length; i < ii; i += 2) {
|
|
|
|
const x = points[i];
|
|
|
|
points[i] = trX - points[i + 1];
|
|
|
|
points[i + 1] = trY - x;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error("Invalid rotation");
|
|
|
|
}
|
|
|
|
return points;
|
|
|
|
}
|
|
|
|
|
|
|
|
static #fromPDFCoordinates(points, rect, rotation) {
|
|
|
|
const [blX, blY, trX, trY] = rect;
|
|
|
|
|
|
|
|
switch (rotation) {
|
|
|
|
case 0:
|
|
|
|
for (let i = 0, ii = points.length; i < ii; i += 2) {
|
|
|
|
points[i] -= blX;
|
|
|
|
points[i + 1] = trY - points[i + 1];
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 90:
|
|
|
|
for (let i = 0, ii = points.length; i < ii; i += 2) {
|
|
|
|
const x = points[i];
|
|
|
|
points[i] = points[i + 1] - blY;
|
|
|
|
points[i + 1] = x - blX;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 180:
|
|
|
|
for (let i = 0, ii = points.length; i < ii; i += 2) {
|
|
|
|
points[i] = trX - points[i];
|
|
|
|
points[i + 1] -= blY;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 270:
|
|
|
|
for (let i = 0, ii = points.length; i < ii; i += 2) {
|
|
|
|
const x = points[i];
|
|
|
|
points[i] = trY - points[i + 1];
|
|
|
|
points[i + 1] = trX - x;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error("Invalid rotation");
|
|
|
|
}
|
|
|
|
return points;
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
/**
|
|
|
|
* Transform and serialize the paths.
|
|
|
|
* @param {number} s - scale factor
|
|
|
|
* @param {number} tx - abscissa of the translation
|
|
|
|
* @param {number} ty - ordinate of the translation
|
2023-05-30 18:24:49 +02:00
|
|
|
* @param {Array<number>} rect - the bounding box of the annotation
|
2022-06-04 23:28:19 +02:00
|
|
|
*/
|
2023-05-30 18:24:49 +02:00
|
|
|
#serializePaths(s, tx, ty, rect) {
|
2022-06-04 23:28:19 +02:00
|
|
|
const paths = [];
|
|
|
|
const padding = this.thickness / 2;
|
2023-05-30 18:24:49 +02:00
|
|
|
const shiftX = s * tx + padding;
|
|
|
|
const shiftY = s * ty + padding;
|
2022-06-04 23:28:19 +02:00
|
|
|
for (const bezier of this.paths) {
|
2023-05-30 18:24:49 +02:00
|
|
|
const buffer = [];
|
|
|
|
const points = [];
|
|
|
|
for (let j = 0, jj = bezier.length; j < jj; j++) {
|
|
|
|
const [first, control1, control2, second] = bezier[j];
|
|
|
|
const p10 = s * first[0] + shiftX;
|
|
|
|
const p11 = s * first[1] + shiftY;
|
|
|
|
const p20 = s * control1[0] + shiftX;
|
|
|
|
const p21 = s * control1[1] + shiftY;
|
|
|
|
const p30 = s * control2[0] + shiftX;
|
|
|
|
const p31 = s * control2[1] + shiftY;
|
|
|
|
const p40 = s * second[0] + shiftX;
|
|
|
|
const p41 = s * second[1] + shiftY;
|
|
|
|
|
|
|
|
if (j === 0) {
|
2022-06-04 23:28:19 +02:00
|
|
|
buffer.push(p10, p11);
|
|
|
|
points.push(p10, p11);
|
|
|
|
}
|
|
|
|
buffer.push(p20, p21, p30, p31, p40, p41);
|
2023-05-30 18:24:49 +02:00
|
|
|
points.push(p20, p21);
|
|
|
|
if (j === jj - 1) {
|
|
|
|
points.push(p40, p41);
|
|
|
|
}
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
2023-05-30 18:24:49 +02:00
|
|
|
paths.push({
|
|
|
|
bezier: InkEditor.#toPDFCoordinates(buffer, rect, this.rotation),
|
|
|
|
points: InkEditor.#toPDFCoordinates(points, rect, this.rotation),
|
|
|
|
});
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return paths;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the bounding box containing all the paths.
|
|
|
|
* @returns {Array<number>}
|
|
|
|
*/
|
|
|
|
#getBbox() {
|
|
|
|
let xMin = Infinity;
|
|
|
|
let xMax = -Infinity;
|
|
|
|
let yMin = Infinity;
|
|
|
|
let yMax = -Infinity;
|
|
|
|
|
|
|
|
for (const path of this.paths) {
|
|
|
|
for (const [first, control1, control2, second] of path) {
|
|
|
|
const bbox = Util.bezierBoundingBox(
|
|
|
|
...first,
|
|
|
|
...control1,
|
|
|
|
...control2,
|
|
|
|
...second
|
|
|
|
);
|
|
|
|
xMin = Math.min(xMin, bbox[0]);
|
|
|
|
yMin = Math.min(yMin, bbox[1]);
|
|
|
|
xMax = Math.max(xMax, bbox[2]);
|
|
|
|
yMax = Math.max(yMax, bbox[3]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return [xMin, yMin, xMax, yMax];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The bounding box is computed with null thickness, so we must take
|
|
|
|
* it into account for the display.
|
|
|
|
* It corresponds to the total padding, hence it should be divided by 2
|
|
|
|
* in order to have left/right paddings.
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
#getPadding() {
|
2022-07-19 17:33:26 +02:00
|
|
|
return this.#disableEditing
|
2022-12-05 12:25:06 +01:00
|
|
|
? Math.ceil(this.thickness * this.parentScale)
|
2022-07-19 17:33:26 +02:00
|
|
|
: 0;
|
2022-06-04 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the div position and dimensions in order to fit to
|
|
|
|
* the bounding box of the contents.
|
|
|
|
* @returns {undefined}
|
|
|
|
*/
|
2022-07-29 16:53:03 +02:00
|
|
|
#fitToContent(firstTime = false) {
|
2022-06-04 23:28:19 +02:00
|
|
|
if (this.isEmpty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.#disableEditing) {
|
|
|
|
this.#redraw();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const bbox = this.#getBbox();
|
|
|
|
const padding = this.#getPadding();
|
2023-07-05 18:09:53 +02:00
|
|
|
this.#baseWidth = Math.max(AnnotationEditor.MIN_SIZE, bbox[2] - bbox[0]);
|
|
|
|
this.#baseHeight = Math.max(AnnotationEditor.MIN_SIZE, bbox[3] - bbox[1]);
|
2022-06-04 23:28:19 +02:00
|
|
|
|
|
|
|
const width = Math.ceil(padding + this.#baseWidth * this.scaleFactor);
|
|
|
|
const height = Math.ceil(padding + this.#baseHeight * this.scaleFactor);
|
|
|
|
|
2022-12-05 12:25:06 +01:00
|
|
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
2022-06-23 15:47:45 +02:00
|
|
|
this.width = width / parentWidth;
|
|
|
|
this.height = height / parentHeight;
|
2022-06-04 23:28:19 +02:00
|
|
|
|
2023-07-05 18:09:53 +02:00
|
|
|
this.setAspectRatio(width, height);
|
2022-06-04 23:28:19 +02:00
|
|
|
|
|
|
|
const prevTranslationX = this.translationX;
|
|
|
|
const prevTranslationY = this.translationY;
|
|
|
|
|
|
|
|
this.translationX = -bbox[0];
|
|
|
|
this.translationY = -bbox[1];
|
|
|
|
this.#setCanvasDims();
|
|
|
|
this.#redraw();
|
|
|
|
|
2022-07-07 18:39:10 +02:00
|
|
|
this.#realWidth = width;
|
|
|
|
this.#realHeight = height;
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
this.setDims(width, height);
|
2022-07-29 16:53:03 +02:00
|
|
|
const unscaledPadding = firstTime ? padding / this.scaleFactor / 2 : 0;
|
2022-06-04 23:28:19 +02:00
|
|
|
this.translate(
|
2022-07-19 17:33:26 +02:00
|
|
|
prevTranslationX - this.translationX - unscaledPadding,
|
|
|
|
prevTranslationY - this.translationY - unscaledPadding
|
2022-06-04 23:28:19 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-07-18 14:47:09 +02:00
|
|
|
/** @inheritdoc */
|
2022-12-05 12:25:06 +01:00
|
|
|
static deserialize(data, parent, uiManager) {
|
2023-06-05 11:32:44 +02:00
|
|
|
if (data instanceof InkAnnotationElement) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-12-05 12:25:06 +01:00
|
|
|
const editor = super.deserialize(data, parent, uiManager);
|
2022-07-18 14:47:09 +02:00
|
|
|
|
|
|
|
editor.thickness = data.thickness;
|
|
|
|
editor.color = Util.makeHexColor(...data.color);
|
2022-07-24 22:19:09 +02:00
|
|
|
editor.opacity = data.opacity;
|
2022-07-18 14:47:09 +02:00
|
|
|
|
2022-12-05 12:25:06 +01:00
|
|
|
const [pageWidth, pageHeight] = editor.pageDimensions;
|
2022-07-18 14:47:09 +02:00
|
|
|
const width = editor.width * pageWidth;
|
|
|
|
const height = editor.height * pageHeight;
|
2022-12-05 12:25:06 +01:00
|
|
|
const scaleFactor = editor.parentScale;
|
2022-07-18 14:47:09 +02:00
|
|
|
const padding = data.thickness / 2;
|
|
|
|
|
|
|
|
editor.#disableEditing = true;
|
|
|
|
editor.#realWidth = Math.round(width);
|
|
|
|
editor.#realHeight = Math.round(height);
|
|
|
|
|
2023-05-30 18:24:49 +02:00
|
|
|
const { paths, rect, rotation } = data;
|
|
|
|
|
|
|
|
for (let { bezier } of paths) {
|
|
|
|
bezier = InkEditor.#fromPDFCoordinates(bezier, rect, rotation);
|
2022-07-18 14:47:09 +02:00
|
|
|
const path = [];
|
|
|
|
editor.paths.push(path);
|
|
|
|
let p0 = scaleFactor * (bezier[0] - padding);
|
2023-05-30 18:24:49 +02:00
|
|
|
let p1 = scaleFactor * (bezier[1] - padding);
|
2022-07-18 14:47:09 +02:00
|
|
|
for (let i = 2, ii = bezier.length; i < ii; i += 6) {
|
|
|
|
const p10 = scaleFactor * (bezier[i] - padding);
|
2023-05-30 18:24:49 +02:00
|
|
|
const p11 = scaleFactor * (bezier[i + 1] - padding);
|
2022-07-18 14:47:09 +02:00
|
|
|
const p20 = scaleFactor * (bezier[i + 2] - padding);
|
2023-05-30 18:24:49 +02:00
|
|
|
const p21 = scaleFactor * (bezier[i + 3] - padding);
|
2022-07-18 14:47:09 +02:00
|
|
|
const p30 = scaleFactor * (bezier[i + 4] - padding);
|
2023-05-30 18:24:49 +02:00
|
|
|
const p31 = scaleFactor * (bezier[i + 5] - padding);
|
2022-07-18 14:47:09 +02:00
|
|
|
path.push([
|
|
|
|
[p0, p1],
|
|
|
|
[p10, p11],
|
|
|
|
[p20, p21],
|
|
|
|
[p30, p31],
|
|
|
|
]);
|
|
|
|
p0 = p30;
|
|
|
|
p1 = p31;
|
|
|
|
}
|
|
|
|
const path2D = this.#buildPath2D(path);
|
|
|
|
editor.bezierPath2D.push(path2D);
|
|
|
|
}
|
|
|
|
|
|
|
|
const bbox = editor.#getBbox();
|
2023-07-05 18:09:53 +02:00
|
|
|
editor.#baseWidth = Math.max(AnnotationEditor.MIN_SIZE, bbox[2] - bbox[0]);
|
|
|
|
editor.#baseHeight = Math.max(AnnotationEditor.MIN_SIZE, bbox[3] - bbox[1]);
|
2022-07-18 14:47:09 +02:00
|
|
|
editor.#setScaleFactor(width, height);
|
|
|
|
|
|
|
|
return editor;
|
|
|
|
}
|
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
/** @inheritdoc */
|
|
|
|
serialize() {
|
2022-07-12 17:32:14 +02:00
|
|
|
if (this.isEmpty()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-06-23 15:47:45 +02:00
|
|
|
const rect = this.getRect(0, 0);
|
2022-06-29 15:39:02 +02:00
|
|
|
const color = AnnotationEditor._colorManager.convert(this.ctx.strokeStyle);
|
2022-06-13 18:23:10 +02:00
|
|
|
|
2022-06-04 23:28:19 +02:00
|
|
|
return {
|
|
|
|
annotationType: AnnotationEditorType.INK,
|
2022-06-13 18:23:10 +02:00
|
|
|
color,
|
2022-06-04 23:28:19 +02:00
|
|
|
thickness: this.thickness,
|
2022-07-24 22:19:09 +02:00
|
|
|
opacity: this.opacity,
|
2022-06-04 23:28:19 +02:00
|
|
|
paths: this.#serializePaths(
|
2022-12-05 12:25:06 +01:00
|
|
|
this.scaleFactor / this.parentScale,
|
2022-06-04 23:28:19 +02:00
|
|
|
this.translationX,
|
|
|
|
this.translationY,
|
2023-05-30 18:24:49 +02:00
|
|
|
rect
|
2022-06-04 23:28:19 +02:00
|
|
|
),
|
2022-12-05 12:25:06 +01:00
|
|
|
pageIndex: this.pageIndex,
|
2022-06-23 15:47:45 +02:00
|
|
|
rect,
|
|
|
|
rotation: this.rotation,
|
2022-06-04 23:28:19 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-09 11:16:10 +01:00
|
|
|
export { InkEditor };
|