ink.js 22 KB

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