ink.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. /**
  2. * @licstart The following is the entire license notice for the
  3. * JavaScript code in this page
  4. *
  5. * Copyright 2022 Mozilla Foundation
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. *
  19. * @licend The above is the entire license notice for the
  20. * JavaScript code in this page
  21. */
  22. "use strict";
  23. Object.defineProperty(exports, "__esModule", {
  24. value: true
  25. });
  26. exports.InkEditor = void 0;
  27. Object.defineProperty(exports, "fitCurve", {
  28. enumerable: true,
  29. get: function () {
  30. return _fit_curve.fitCurve;
  31. }
  32. });
  33. var _util = require("../../shared/util.js");
  34. var _editor = require("./editor.js");
  35. var _fit_curve = require("./fit_curve");
  36. var _tools = require("./tools.js");
  37. const RESIZER_SIZE = 16;
  38. const TIME_TO_WAIT_BEFORE_FIXING_DIMS = 100;
  39. class InkEditor extends _editor.AnnotationEditor {
  40. #aspectRatio = 0;
  41. #baseHeight = 0;
  42. #baseWidth = 0;
  43. #boundCanvasPointermove = this.canvasPointermove.bind(this);
  44. #boundCanvasPointerleave = this.canvasPointerleave.bind(this);
  45. #boundCanvasPointerup = this.canvasPointerup.bind(this);
  46. #boundCanvasPointerdown = this.canvasPointerdown.bind(this);
  47. #disableEditing = false;
  48. #isCanvasInitialized = false;
  49. #lastPoint = null;
  50. #observer = null;
  51. #realWidth = 0;
  52. #realHeight = 0;
  53. #requestFrameCallback = null;
  54. static _defaultColor = null;
  55. static _defaultOpacity = 1;
  56. static _defaultThickness = 1;
  57. static _l10nPromise;
  58. static _type = "ink";
  59. constructor(params) {
  60. super({
  61. ...params,
  62. name: "inkEditor"
  63. });
  64. this.color = params.color || null;
  65. this.thickness = params.thickness || null;
  66. this.opacity = params.opacity || null;
  67. this.paths = [];
  68. this.bezierPath2D = [];
  69. this.currentPath = [];
  70. this.scaleFactor = 1;
  71. this.translationX = this.translationY = 0;
  72. this.x = 0;
  73. this.y = 0;
  74. }
  75. static initialize(l10n) {
  76. this._l10nPromise = new Map(["editor_ink_canvas_aria_label", "editor_ink2_aria_label"].map(str => [str, l10n.get(str)]));
  77. }
  78. static updateDefaultParams(type, value) {
  79. switch (type) {
  80. case _util.AnnotationEditorParamsType.INK_THICKNESS:
  81. InkEditor._defaultThickness = value;
  82. break;
  83. case _util.AnnotationEditorParamsType.INK_COLOR:
  84. InkEditor._defaultColor = value;
  85. break;
  86. case _util.AnnotationEditorParamsType.INK_OPACITY:
  87. InkEditor._defaultOpacity = value / 100;
  88. break;
  89. }
  90. }
  91. updateParams(type, value) {
  92. switch (type) {
  93. case _util.AnnotationEditorParamsType.INK_THICKNESS:
  94. this.#updateThickness(value);
  95. break;
  96. case _util.AnnotationEditorParamsType.INK_COLOR:
  97. this.#updateColor(value);
  98. break;
  99. case _util.AnnotationEditorParamsType.INK_OPACITY:
  100. this.#updateOpacity(value);
  101. break;
  102. }
  103. }
  104. static get defaultPropertiesToUpdate() {
  105. return [[_util.AnnotationEditorParamsType.INK_THICKNESS, InkEditor._defaultThickness], [_util.AnnotationEditorParamsType.INK_COLOR, InkEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor], [_util.AnnotationEditorParamsType.INK_OPACITY, Math.round(InkEditor._defaultOpacity * 100)]];
  106. }
  107. get propertiesToUpdate() {
  108. return [[_util.AnnotationEditorParamsType.INK_THICKNESS, this.thickness || InkEditor._defaultThickness], [_util.AnnotationEditorParamsType.INK_COLOR, this.color || InkEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor], [_util.AnnotationEditorParamsType.INK_OPACITY, Math.round(100 * (this.opacity ?? InkEditor._defaultOpacity))]];
  109. }
  110. #updateThickness(thickness) {
  111. const savedThickness = this.thickness;
  112. this.addCommands({
  113. cmd: () => {
  114. this.thickness = thickness;
  115. this.#fitToContent();
  116. },
  117. undo: () => {
  118. this.thickness = savedThickness;
  119. this.#fitToContent();
  120. },
  121. mustExec: true,
  122. type: _util.AnnotationEditorParamsType.INK_THICKNESS,
  123. overwriteIfSameType: true,
  124. keepUndo: true
  125. });
  126. }
  127. #updateColor(color) {
  128. const savedColor = this.color;
  129. this.addCommands({
  130. cmd: () => {
  131. this.color = color;
  132. this.#redraw();
  133. },
  134. undo: () => {
  135. this.color = savedColor;
  136. this.#redraw();
  137. },
  138. mustExec: true,
  139. type: _util.AnnotationEditorParamsType.INK_COLOR,
  140. overwriteIfSameType: true,
  141. keepUndo: true
  142. });
  143. }
  144. #updateOpacity(opacity) {
  145. opacity /= 100;
  146. const savedOpacity = this.opacity;
  147. this.addCommands({
  148. cmd: () => {
  149. this.opacity = opacity;
  150. this.#redraw();
  151. },
  152. undo: () => {
  153. this.opacity = savedOpacity;
  154. this.#redraw();
  155. },
  156. mustExec: true,
  157. type: _util.AnnotationEditorParamsType.INK_OPACITY,
  158. overwriteIfSameType: true,
  159. keepUndo: true
  160. });
  161. }
  162. rebuild() {
  163. super.rebuild();
  164. if (this.div === null) {
  165. return;
  166. }
  167. if (!this.canvas) {
  168. this.#createCanvas();
  169. this.#createObserver();
  170. }
  171. if (!this.isAttachedToDOM) {
  172. this.parent.add(this);
  173. this.#setCanvasDims();
  174. }
  175. this.#fitToContent();
  176. }
  177. remove() {
  178. if (this.canvas === null) {
  179. return;
  180. }
  181. if (!this.isEmpty()) {
  182. this.commit();
  183. }
  184. this.canvas.width = this.canvas.height = 0;
  185. this.canvas.remove();
  186. this.canvas = null;
  187. this.#observer.disconnect();
  188. this.#observer = null;
  189. super.remove();
  190. }
  191. setParent(parent) {
  192. if (!this.parent && parent) {
  193. this._uiManager.removeShouldRescale(this);
  194. } else if (this.parent && parent === null) {
  195. this._uiManager.addShouldRescale(this);
  196. }
  197. super.setParent(parent);
  198. }
  199. onScaleChanging() {
  200. const [parentWidth, parentHeight] = this.parentDimensions;
  201. const width = this.width * parentWidth;
  202. const height = this.height * parentHeight;
  203. this.setDimensions(width, height);
  204. }
  205. enableEditMode() {
  206. if (this.#disableEditing || this.canvas === null) {
  207. return;
  208. }
  209. super.enableEditMode();
  210. this.div.draggable = false;
  211. this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown);
  212. this.canvas.addEventListener("pointerup", this.#boundCanvasPointerup);
  213. }
  214. disableEditMode() {
  215. if (!this.isInEditMode() || this.canvas === null) {
  216. return;
  217. }
  218. super.disableEditMode();
  219. this.div.draggable = !this.isEmpty();
  220. this.div.classList.remove("editing");
  221. this.canvas.removeEventListener("pointerdown", this.#boundCanvasPointerdown);
  222. this.canvas.removeEventListener("pointerup", this.#boundCanvasPointerup);
  223. }
  224. onceAdded() {
  225. this.div.draggable = !this.isEmpty();
  226. }
  227. isEmpty() {
  228. return this.paths.length === 0 || this.paths.length === 1 && this.paths[0].length === 0;
  229. }
  230. #getInitialBBox() {
  231. const {
  232. parentRotation,
  233. parentDimensions: [width, height]
  234. } = this;
  235. switch (parentRotation) {
  236. case 90:
  237. return [0, height, height, width];
  238. case 180:
  239. return [width, height, width, height];
  240. case 270:
  241. return [width, 0, height, width];
  242. default:
  243. return [0, 0, width, height];
  244. }
  245. }
  246. #setStroke() {
  247. const {
  248. ctx,
  249. color,
  250. opacity,
  251. thickness,
  252. parentScale,
  253. scaleFactor
  254. } = this;
  255. ctx.lineWidth = thickness * parentScale / scaleFactor;
  256. ctx.lineCap = "round";
  257. ctx.lineJoin = "round";
  258. ctx.miterLimit = 10;
  259. ctx.strokeStyle = `${color}${(0, _tools.opacityToHex)(opacity)}`;
  260. }
  261. #startDrawing(x, y) {
  262. this.isEditing = true;
  263. if (!this.#isCanvasInitialized) {
  264. this.#isCanvasInitialized = true;
  265. this.#setCanvasDims();
  266. this.thickness ||= InkEditor._defaultThickness;
  267. this.color ||= InkEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor;
  268. this.opacity ??= InkEditor._defaultOpacity;
  269. }
  270. this.currentPath.push([x, y]);
  271. this.#lastPoint = null;
  272. this.#setStroke();
  273. this.ctx.beginPath();
  274. this.ctx.moveTo(x, y);
  275. this.#requestFrameCallback = () => {
  276. if (!this.#requestFrameCallback) {
  277. return;
  278. }
  279. if (this.#lastPoint) {
  280. if (this.isEmpty()) {
  281. this.ctx.setTransform(1, 0, 0, 1, 0, 0);
  282. this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  283. } else {
  284. this.#redraw();
  285. }
  286. this.ctx.lineTo(...this.#lastPoint);
  287. this.#lastPoint = null;
  288. this.ctx.stroke();
  289. }
  290. window.requestAnimationFrame(this.#requestFrameCallback);
  291. };
  292. window.requestAnimationFrame(this.#requestFrameCallback);
  293. }
  294. #draw(x, y) {
  295. const [lastX, lastY] = this.currentPath.at(-1);
  296. if (x === lastX && y === lastY) {
  297. return;
  298. }
  299. this.currentPath.push([x, y]);
  300. this.#lastPoint = [x, y];
  301. }
  302. #stopDrawing(x, y) {
  303. this.ctx.closePath();
  304. this.#requestFrameCallback = null;
  305. x = Math.min(Math.max(x, 0), this.canvas.width);
  306. y = Math.min(Math.max(y, 0), this.canvas.height);
  307. const [lastX, lastY] = this.currentPath.at(-1);
  308. if (x !== lastX || y !== lastY) {
  309. this.currentPath.push([x, y]);
  310. }
  311. let bezier;
  312. if (this.currentPath.length !== 1) {
  313. bezier = (0, _fit_curve.fitCurve)(this.currentPath, 30, null);
  314. } else {
  315. const xy = [x, y];
  316. bezier = [[xy, xy.slice(), xy.slice(), xy]];
  317. }
  318. const path2D = InkEditor.#buildPath2D(bezier);
  319. this.currentPath.length = 0;
  320. const cmd = () => {
  321. this.paths.push(bezier);
  322. this.bezierPath2D.push(path2D);
  323. this.rebuild();
  324. };
  325. const undo = () => {
  326. this.paths.pop();
  327. this.bezierPath2D.pop();
  328. if (this.paths.length === 0) {
  329. this.remove();
  330. } else {
  331. if (!this.canvas) {
  332. this.#createCanvas();
  333. this.#createObserver();
  334. }
  335. this.#fitToContent();
  336. }
  337. };
  338. this.addCommands({
  339. cmd,
  340. undo,
  341. mustExec: true
  342. });
  343. }
  344. #redraw() {
  345. if (this.isEmpty()) {
  346. this.#updateTransform();
  347. return;
  348. }
  349. this.#setStroke();
  350. const {
  351. canvas,
  352. ctx
  353. } = this;
  354. ctx.setTransform(1, 0, 0, 1, 0, 0);
  355. ctx.clearRect(0, 0, canvas.width, canvas.height);
  356. this.#updateTransform();
  357. for (const path of this.bezierPath2D) {
  358. ctx.stroke(path);
  359. }
  360. }
  361. commit() {
  362. if (this.#disableEditing) {
  363. return;
  364. }
  365. super.commit();
  366. this.isEditing = false;
  367. this.disableEditMode();
  368. this.setInForeground();
  369. this.#disableEditing = true;
  370. this.div.classList.add("disabled");
  371. this.#fitToContent(true);
  372. this.parent.addInkEditorIfNeeded(true);
  373. this.parent.moveEditorInDOM(this);
  374. this.div.focus({
  375. preventScroll: true
  376. });
  377. }
  378. focusin(event) {
  379. super.focusin(event);
  380. this.enableEditMode();
  381. }
  382. canvasPointerdown(event) {
  383. if (event.button !== 0 || !this.isInEditMode() || this.#disableEditing) {
  384. return;
  385. }
  386. this.setInForeground();
  387. if (event.type !== "mouse") {
  388. this.div.focus();
  389. }
  390. event.stopPropagation();
  391. this.canvas.addEventListener("pointerleave", this.#boundCanvasPointerleave);
  392. this.canvas.addEventListener("pointermove", this.#boundCanvasPointermove);
  393. this.#startDrawing(event.offsetX, event.offsetY);
  394. }
  395. canvasPointermove(event) {
  396. event.stopPropagation();
  397. this.#draw(event.offsetX, event.offsetY);
  398. }
  399. canvasPointerup(event) {
  400. if (event.button !== 0) {
  401. return;
  402. }
  403. if (this.isInEditMode() && this.currentPath.length !== 0) {
  404. event.stopPropagation();
  405. this.#endDrawing(event);
  406. this.setInBackground();
  407. }
  408. }
  409. canvasPointerleave(event) {
  410. this.#endDrawing(event);
  411. this.setInBackground();
  412. }
  413. #endDrawing(event) {
  414. this.#stopDrawing(event.offsetX, event.offsetY);
  415. this.canvas.removeEventListener("pointerleave", this.#boundCanvasPointerleave);
  416. this.canvas.removeEventListener("pointermove", this.#boundCanvasPointermove);
  417. this.addToAnnotationStorage();
  418. }
  419. #createCanvas() {
  420. this.canvas = document.createElement("canvas");
  421. this.canvas.width = this.canvas.height = 0;
  422. this.canvas.className = "inkEditorCanvas";
  423. InkEditor._l10nPromise.get("editor_ink_canvas_aria_label").then(msg => this.canvas?.setAttribute("aria-label", msg));
  424. this.div.append(this.canvas);
  425. this.ctx = this.canvas.getContext("2d");
  426. }
  427. #createObserver() {
  428. let timeoutId = null;
  429. this.#observer = new ResizeObserver(entries => {
  430. const rect = entries[0].contentRect;
  431. if (rect.width && rect.height) {
  432. if (timeoutId !== null) {
  433. clearTimeout(timeoutId);
  434. }
  435. timeoutId = setTimeout(() => {
  436. this.fixDims();
  437. timeoutId = null;
  438. }, TIME_TO_WAIT_BEFORE_FIXING_DIMS);
  439. this.setDimensions(rect.width, rect.height);
  440. }
  441. });
  442. this.#observer.observe(this.div);
  443. }
  444. render() {
  445. if (this.div) {
  446. return this.div;
  447. }
  448. let baseX, baseY;
  449. if (this.width) {
  450. baseX = this.x;
  451. baseY = this.y;
  452. }
  453. super.render();
  454. InkEditor._l10nPromise.get("editor_ink2_aria_label").then(msg => this.div?.setAttribute("aria-label", msg));
  455. const [x, y, w, h] = this.#getInitialBBox();
  456. this.setAt(x, y, 0, 0);
  457. this.setDims(w, h);
  458. this.#createCanvas();
  459. if (this.width) {
  460. const [parentWidth, parentHeight] = this.parentDimensions;
  461. this.setAt(baseX * parentWidth, baseY * parentHeight, this.width * parentWidth, this.height * parentHeight);
  462. this.#isCanvasInitialized = true;
  463. this.#setCanvasDims();
  464. this.setDims(this.width * parentWidth, this.height * parentHeight);
  465. this.#redraw();
  466. this.#setMinDims();
  467. this.div.classList.add("disabled");
  468. } else {
  469. this.div.classList.add("editing");
  470. this.enableEditMode();
  471. }
  472. this.#createObserver();
  473. return this.div;
  474. }
  475. #setCanvasDims() {
  476. if (!this.#isCanvasInitialized) {
  477. return;
  478. }
  479. const [parentWidth, parentHeight] = this.parentDimensions;
  480. this.canvas.width = Math.ceil(this.width * parentWidth);
  481. this.canvas.height = Math.ceil(this.height * parentHeight);
  482. this.#updateTransform();
  483. }
  484. setDimensions(width, height) {
  485. const roundedWidth = Math.round(width);
  486. const roundedHeight = Math.round(height);
  487. if (this.#realWidth === roundedWidth && this.#realHeight === roundedHeight) {
  488. return;
  489. }
  490. this.#realWidth = roundedWidth;
  491. this.#realHeight = roundedHeight;
  492. this.canvas.style.visibility = "hidden";
  493. if (this.#aspectRatio && Math.abs(this.#aspectRatio - width / height) > 1e-2) {
  494. height = Math.ceil(width / this.#aspectRatio);
  495. this.setDims(width, height);
  496. }
  497. const [parentWidth, parentHeight] = this.parentDimensions;
  498. this.width = width / parentWidth;
  499. this.height = height / parentHeight;
  500. if (this.#disableEditing) {
  501. this.#setScaleFactor(width, height);
  502. }
  503. this.#setCanvasDims();
  504. this.#redraw();
  505. this.canvas.style.visibility = "visible";
  506. }
  507. #setScaleFactor(width, height) {
  508. const padding = this.#getPadding();
  509. const scaleFactorW = (width - padding) / this.#baseWidth;
  510. const scaleFactorH = (height - padding) / this.#baseHeight;
  511. this.scaleFactor = Math.min(scaleFactorW, scaleFactorH);
  512. }
  513. #updateTransform() {
  514. const padding = this.#getPadding() / 2;
  515. this.ctx.setTransform(this.scaleFactor, 0, 0, this.scaleFactor, this.translationX * this.scaleFactor + padding, this.translationY * this.scaleFactor + padding);
  516. }
  517. static #buildPath2D(bezier) {
  518. const path2D = new Path2D();
  519. for (let i = 0, ii = bezier.length; i < ii; i++) {
  520. const [first, control1, control2, second] = bezier[i];
  521. if (i === 0) {
  522. path2D.moveTo(...first);
  523. }
  524. path2D.bezierCurveTo(control1[0], control1[1], control2[0], control2[1], second[0], second[1]);
  525. }
  526. return path2D;
  527. }
  528. #serializePaths(s, tx, ty, h) {
  529. const NUMBER_OF_POINTS_ON_BEZIER_CURVE = 4;
  530. const paths = [];
  531. const padding = this.thickness / 2;
  532. let buffer, points;
  533. for (const bezier of this.paths) {
  534. buffer = [];
  535. points = [];
  536. for (let i = 0, ii = bezier.length; i < ii; i++) {
  537. const [first, control1, control2, second] = bezier[i];
  538. const p10 = s * (first[0] + tx) + padding;
  539. const p11 = h - s * (first[1] + ty) - padding;
  540. const p20 = s * (control1[0] + tx) + padding;
  541. const p21 = h - s * (control1[1] + ty) - padding;
  542. const p30 = s * (control2[0] + tx) + padding;
  543. const p31 = h - s * (control2[1] + ty) - padding;
  544. const p40 = s * (second[0] + tx) + padding;
  545. const p41 = h - s * (second[1] + ty) - padding;
  546. if (i === 0) {
  547. buffer.push(p10, p11);
  548. points.push(p10, p11);
  549. }
  550. buffer.push(p20, p21, p30, p31, p40, p41);
  551. this.#extractPointsOnBezier(p10, p11, p20, p21, p30, p31, p40, p41, NUMBER_OF_POINTS_ON_BEZIER_CURVE, points);
  552. }
  553. paths.push({
  554. bezier: buffer,
  555. points
  556. });
  557. }
  558. return paths;
  559. }
  560. #extractPointsOnBezier(p10, p11, p20, p21, p30, p31, p40, p41, n, points) {
  561. if (this.#isAlmostFlat(p10, p11, p20, p21, p30, p31, p40, p41)) {
  562. points.push(p40, p41);
  563. return;
  564. }
  565. for (let i = 1; i < n - 1; i++) {
  566. const t = i / n;
  567. const mt = 1 - t;
  568. let q10 = t * p10 + mt * p20;
  569. let q11 = t * p11 + mt * p21;
  570. let q20 = t * p20 + mt * p30;
  571. let q21 = t * p21 + mt * p31;
  572. const q30 = t * p30 + mt * p40;
  573. const q31 = t * p31 + mt * p41;
  574. q10 = t * q10 + mt * q20;
  575. q11 = t * q11 + mt * q21;
  576. q20 = t * q20 + mt * q30;
  577. q21 = t * q21 + mt * q31;
  578. q10 = t * q10 + mt * q20;
  579. q11 = t * q11 + mt * q21;
  580. points.push(q10, q11);
  581. }
  582. points.push(p40, p41);
  583. }
  584. #isAlmostFlat(p10, p11, p20, p21, p30, p31, p40, p41) {
  585. const tol = 10;
  586. const ax = (3 * p20 - 2 * p10 - p40) ** 2;
  587. const ay = (3 * p21 - 2 * p11 - p41) ** 2;
  588. const bx = (3 * p30 - p10 - 2 * p40) ** 2;
  589. const by = (3 * p31 - p11 - 2 * p41) ** 2;
  590. return Math.max(ax, bx) + Math.max(ay, by) <= tol;
  591. }
  592. #getBbox() {
  593. let xMin = Infinity;
  594. let xMax = -Infinity;
  595. let yMin = Infinity;
  596. let yMax = -Infinity;
  597. for (const path of this.paths) {
  598. for (const [first, control1, control2, second] of path) {
  599. const bbox = _util.Util.bezierBoundingBox(...first, ...control1, ...control2, ...second);
  600. xMin = Math.min(xMin, bbox[0]);
  601. yMin = Math.min(yMin, bbox[1]);
  602. xMax = Math.max(xMax, bbox[2]);
  603. yMax = Math.max(yMax, bbox[3]);
  604. }
  605. }
  606. return [xMin, yMin, xMax, yMax];
  607. }
  608. #getPadding() {
  609. return this.#disableEditing ? Math.ceil(this.thickness * this.parentScale) : 0;
  610. }
  611. #fitToContent(firstTime = false) {
  612. if (this.isEmpty()) {
  613. return;
  614. }
  615. if (!this.#disableEditing) {
  616. this.#redraw();
  617. return;
  618. }
  619. const bbox = this.#getBbox();
  620. const padding = this.#getPadding();
  621. this.#baseWidth = Math.max(RESIZER_SIZE, bbox[2] - bbox[0]);
  622. this.#baseHeight = Math.max(RESIZER_SIZE, bbox[3] - bbox[1]);
  623. const width = Math.ceil(padding + this.#baseWidth * this.scaleFactor);
  624. const height = Math.ceil(padding + this.#baseHeight * this.scaleFactor);
  625. const [parentWidth, parentHeight] = this.parentDimensions;
  626. this.width = width / parentWidth;
  627. this.height = height / parentHeight;
  628. this.#aspectRatio = width / height;
  629. this.#setMinDims();
  630. const prevTranslationX = this.translationX;
  631. const prevTranslationY = this.translationY;
  632. this.translationX = -bbox[0];
  633. this.translationY = -bbox[1];
  634. this.#setCanvasDims();
  635. this.#redraw();
  636. this.#realWidth = width;
  637. this.#realHeight = height;
  638. this.setDims(width, height);
  639. const unscaledPadding = firstTime ? padding / this.scaleFactor / 2 : 0;
  640. this.translate(prevTranslationX - this.translationX - unscaledPadding, prevTranslationY - this.translationY - unscaledPadding);
  641. }
  642. #setMinDims() {
  643. const {
  644. style
  645. } = this.div;
  646. if (this.#aspectRatio >= 1) {
  647. style.minHeight = `${RESIZER_SIZE}px`;
  648. style.minWidth = `${Math.round(this.#aspectRatio * RESIZER_SIZE)}px`;
  649. } else {
  650. style.minWidth = `${RESIZER_SIZE}px`;
  651. style.minHeight = `${Math.round(RESIZER_SIZE / this.#aspectRatio)}px`;
  652. }
  653. }
  654. static deserialize(data, parent, uiManager) {
  655. const editor = super.deserialize(data, parent, uiManager);
  656. editor.thickness = data.thickness;
  657. editor.color = _util.Util.makeHexColor(...data.color);
  658. editor.opacity = data.opacity;
  659. const [pageWidth, pageHeight] = editor.pageDimensions;
  660. const width = editor.width * pageWidth;
  661. const height = editor.height * pageHeight;
  662. const scaleFactor = editor.parentScale;
  663. const padding = data.thickness / 2;
  664. editor.#aspectRatio = width / height;
  665. editor.#disableEditing = true;
  666. editor.#realWidth = Math.round(width);
  667. editor.#realHeight = Math.round(height);
  668. for (const {
  669. bezier
  670. } of data.paths) {
  671. const path = [];
  672. editor.paths.push(path);
  673. let p0 = scaleFactor * (bezier[0] - padding);
  674. let p1 = scaleFactor * (height - bezier[1] - padding);
  675. for (let i = 2, ii = bezier.length; i < ii; i += 6) {
  676. const p10 = scaleFactor * (bezier[i] - padding);
  677. const p11 = scaleFactor * (height - bezier[i + 1] - padding);
  678. const p20 = scaleFactor * (bezier[i + 2] - padding);
  679. const p21 = scaleFactor * (height - bezier[i + 3] - padding);
  680. const p30 = scaleFactor * (bezier[i + 4] - padding);
  681. const p31 = scaleFactor * (height - bezier[i + 5] - padding);
  682. path.push([[p0, p1], [p10, p11], [p20, p21], [p30, p31]]);
  683. p0 = p30;
  684. p1 = p31;
  685. }
  686. const path2D = this.#buildPath2D(path);
  687. editor.bezierPath2D.push(path2D);
  688. }
  689. const bbox = editor.#getBbox();
  690. editor.#baseWidth = Math.max(RESIZER_SIZE, bbox[2] - bbox[0]);
  691. editor.#baseHeight = Math.max(RESIZER_SIZE, bbox[3] - bbox[1]);
  692. editor.#setScaleFactor(width, height);
  693. return editor;
  694. }
  695. serialize() {
  696. if (this.isEmpty()) {
  697. return null;
  698. }
  699. const rect = this.getRect(0, 0);
  700. const height = this.rotation % 180 === 0 ? rect[3] - rect[1] : rect[2] - rect[0];
  701. const color = _editor.AnnotationEditor._colorManager.convert(this.ctx.strokeStyle);
  702. return {
  703. annotationType: _util.AnnotationEditorType.INK,
  704. color,
  705. thickness: this.thickness,
  706. opacity: this.opacity,
  707. paths: this.#serializePaths(this.scaleFactor / this.parentScale, this.translationX, this.translationY, height),
  708. pageIndex: this.pageIndex,
  709. rect,
  710. rotation: this.rotation
  711. };
  712. }
  713. }
  714. exports.InkEditor = InkEditor;