mark-edit-layer.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import FillArea from "../common/fillarea";
  2. import { ColorMap, Mark } from "../common/interfaces";
  3. import Utils from "../common/utils";
  4. import { Point } from "../core/interface";
  5. import Layer from "../core/layer";
  6. import Rect from "../core/rect";
  7. import MarkTool from "./mark-tool";
  8. const TAG = 'MarkEditLayer';
  9. export default class MarkEditLayer extends Layer {
  10. output: HTMLCanvasElement;
  11. outputCtx: CanvasRenderingContext2D;
  12. raw: HTMLImageElement;
  13. page: HTMLImageElement;
  14. map: HTMLImageElement;
  15. mapPixels: Uint32Array;
  16. colorMap: ColorMap;
  17. areaMap: any = [];
  18. areaList: any[];
  19. undoList: any[] = [];
  20. redoList: any[] = [];
  21. cacheList: any[] = [];
  22. marks: Mark[]; // 标记点
  23. innerMarks: InnerMark[] = []; // 仅供内部使用
  24. override get defaultToolKey(): string { return 'mark-edit-mark'; }
  25. constructor(raw: HTMLImageElement, page: HTMLImageElement, map: HTMLImageElement, colorMap: ColorMap, marks: Mark[]) {
  26. super(page.width, page.height);
  27. this.raw = raw;
  28. this.page = page;
  29. this.map = map;
  30. this.colorMap = colorMap;
  31. this.marks = marks;
  32. console.log("marks:", marks);
  33. if (this.marks && this.marks.length > 0) {
  34. this.innerMarks = this.marks.map(m => { return {x: m.x, y: m.y, radius: m.radius, note: m.note, status: 0} });
  35. }
  36. this.init();
  37. this.addTool(new MarkTool(this));
  38. }
  39. init() {
  40. this.output = Utils.createCanvas(this.width, this.height);
  41. this.outputCtx = this.output.getContext('2d');
  42. this.outputCtx.fillStyle = 'white';
  43. this.outputCtx.fillRect(0, 0, this.width, this.height);
  44. this.mapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
  45. this.createAreaMap();
  46. this.drawToOutput();
  47. // undo redo support.
  48. this.undoList = [];
  49. this.redoList = [];
  50. this.cacheList = [];
  51. }
  52. drawToOutput() {
  53. console.time('drawToOutput');
  54. this.outputCtx.fillStyle = 'white';
  55. this.outputCtx.fillRect(0, 0, this.width, this.height);
  56. //fast create output by map
  57. let outData = this.outputCtx.getImageData(0, 0, this.output.width, this.output.height)
  58. let outPixels = new Uint32Array(outData.data.buffer);
  59. let areaIndex;
  60. for (var i = 0; i < outPixels.length; i++) {
  61. areaIndex = this.mapPixels[i];
  62. if (this.colorMap[areaIndex]) {
  63. outPixels[i] = this.colorMap[areaIndex].color;
  64. }
  65. }
  66. this.outputCtx.putImageData(outData, 0, 0);
  67. console.timeEnd('drawToOutput');
  68. }
  69. /**
  70. * Create area map from map pixels.
  71. */
  72. createAreaMap() {
  73. let width = this.width;
  74. let height = this.height;
  75. let floodArr = this.mapPixels;
  76. let areaMap = {};
  77. let i: number, x: number, y: number, color: number;
  78. for (i = 0; i < floodArr.length; i++) {
  79. color = floodArr[i];
  80. if (!color) continue;
  81. x = i % width;
  82. //y = parseInt(i / width);
  83. y = Math.floor(i / width);
  84. if (areaMap[color]) {
  85. areaMap[color].addPoint(x, y);
  86. } else {
  87. areaMap[color] = new FillArea(color, x, y);
  88. }
  89. }
  90. this.areaMap = areaMap;
  91. this.areaList = Object.keys(areaMap).map(key => {
  92. return areaMap[key];
  93. });
  94. }
  95. update(page?: HTMLImageElement, raw?: HTMLImageElement, map?: HTMLImageElement, colorMap?: ColorMap) {
  96. this.raw = raw || this.raw;
  97. this.page = page || this.page;
  98. this.map = map || this.map;
  99. this.invalidate();
  100. }
  101. updateing = false;
  102. editUpdate() {
  103. this.marks = this.innerMarks.filter(m => m.radius > 0)
  104. .map(m => {
  105. return {x: m.x, y: m.y, radius: m.radius, note: m.note};
  106. });
  107. // 控制下触发频率
  108. if (this.updateing) return;
  109. this.updateing = true;
  110. setTimeout(() => {
  111. this.trigger('edit-update');
  112. this.updateing = false;
  113. }, 1000);
  114. }
  115. get curMarkIdx(): number {
  116. return this.getSelectedMarkIdx();
  117. }
  118. set curMarkIdx(idx: number) {
  119. if (idx >= 0 && idx < this.innerMarks.length) {
  120. this.selectMarkIdx(idx);
  121. }
  122. }
  123. // 获取标注文字
  124. getMarkNote(idx: number) {
  125. if (idx >= 0 && idx < this.innerMarks.length) {
  126. let mark = this.innerMarks[idx];
  127. return mark.note;
  128. }
  129. return null;
  130. }
  131. // 设置标注文字
  132. setMarkNote(idx: number, note: string) {
  133. if (idx >= 0 && idx < this.innerMarks.length) {
  134. this.innerMarks[idx].note = note;
  135. this.marks[idx].note = note;
  136. this.trigger('edit-update');
  137. }
  138. }
  139. /**
  140. * Draw this layer.
  141. * @Override Layer#draw(ctx)
  142. */
  143. override draw(ctx) {
  144. ctx.imageSmoothingEnabled = this.smoothing;
  145. ctx.fillStyle = "white";
  146. ctx.fillRect(0, 0, this.width, this.height);
  147. ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0, 0, this.width, this.height);
  148. if (this.raw) {
  149. ctx.drawImage(this.raw, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
  150. } else {
  151. ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
  152. }
  153. this.drawMarks(ctx);
  154. }
  155. // 生成标注效果图
  156. getMarkBlob(): Promise<Blob> {
  157. let scale = 2;
  158. let canvas = Utils.createCanvas(this.width / scale, this.height / scale); // 压缩下,变为原来的一半大小即可,节省空间
  159. let ctx = canvas.getContext('2d');
  160. ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0, 0, canvas.width, canvas.height);
  161. if (this.raw) {
  162. ctx.drawImage(this.raw, 0, 0, this.page.width, this.page.height, 0, 0, canvas.width, canvas.height);
  163. } else {
  164. ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, canvas.width, canvas.height);
  165. }
  166. this.drawMarksOut(ctx, scale);
  167. return new Promise((done, reject) => { canvas.toBlob(b => { done(b) }) });
  168. }
  169. // 创建圈圈标记
  170. createMark(x: number, y: number, radius: number): InnerMark {
  171. let mark = {x, y, radius, note: '', status: 0};
  172. this.innerMarks.push(mark);
  173. this.selectMark(mark);
  174. this.invalidate();
  175. this.editUpdate();
  176. return mark;
  177. }
  178. // 移动圈圈
  179. moveMark(mark: InnerMark, x: number, y: number) {
  180. mark.x = x;
  181. mark.y = y;
  182. this.editUpdate();
  183. this.invalidate();
  184. }
  185. // 调整圈圈大小
  186. resizeMark(mark: InnerMark, radius: number) {
  187. mark.radius = radius;
  188. this.editUpdate();
  189. this.invalidate();
  190. }
  191. // 选中某个圈圈
  192. selectMark(mark: InnerMark) {
  193. for (let m of this.innerMarks) {
  194. if (m == mark) {
  195. m.status = 1;
  196. } else {
  197. m.status = 0;
  198. }
  199. }
  200. this.invalidate();
  201. }
  202. // 选中指定下标的圈圈
  203. selectMarkIdx(idx: number) {
  204. if (idx >= 0 && idx < this.innerMarks.length) {
  205. let mark = this.innerMarks[idx];
  206. this.selectMark(mark);
  207. }
  208. }
  209. // 全部置为非选中
  210. unselectMarks() {
  211. this.innerMarks.forEach(m => {
  212. m.status = 0;
  213. })
  214. this.invalidate();
  215. }
  216. // 获取当前选中
  217. getSelectedMark(): InnerMark {
  218. return this.innerMarks.find(m => m.status == 1)
  219. }
  220. // 获取当前选中圈圈下标
  221. getSelectedMarkIdx(): number {
  222. return this.innerMarks.findIndex(m => m.status == 1)
  223. }
  224. // 删除指定
  225. deleteSelectedMark() {
  226. let idx = this.innerMarks.findIndex(m => m.status == 1);
  227. if (idx >= 0) {
  228. this.innerMarks.splice(idx, 1);
  229. this.editUpdate();
  230. this.invalidate();
  231. }
  232. }
  233. // 判断某点是否落在某个圈圈标记内
  234. checkInMarks(x: number, y: number): InnerMark {
  235. let dx, dy;
  236. for (let mark of this.innerMarks) {
  237. dx = mark.x - x;
  238. dy = mark.y - y;
  239. if (mark.radius * mark.radius >= dx * dx + dy * dy) { // in circle
  240. return mark;
  241. }
  242. }
  243. return null;
  244. }
  245. // 判断某点落在哪个象限(正方形选框之内和mark圈圈之外的4个角,右上/左上/左下/右下分别对应1/2/3/4象限,对应方位东北/西北/西南/东南)
  246. checkQuadrant(mark: InnerMark, x: number, y: number): number {
  247. let dx = mark.x - x
  248. let dy = mark.y - y;
  249. if (mark.radius * mark.radius >= dx * dx + dy * dy) { // in circle
  250. return 0;
  251. }
  252. let rect = new Rect(mark.x - mark.radius, mark.y - mark.radius, mark.x + mark.radius, mark.y + mark.radius);
  253. if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { // out of square
  254. return 0;
  255. }
  256. if (x > mark.x && y < mark.y) return 1;
  257. if (x < mark.x && y < mark.y) return 2;
  258. if (x < mark.x && y > mark.y) return 3;
  259. if (x > mark.x && y > mark.y) return 4;
  260. return 0;
  261. }
  262. /**
  263. * 绘制标记圈圈
  264. * @param ctx
  265. */
  266. drawMarks(ctx: CanvasRenderingContext2D) {
  267. ctx.save();
  268. ctx.strokeStyle = '#00cc00';
  269. ctx.lineWidth = 8;
  270. this.innerMarks.forEach(m => {
  271. ctx.beginPath();
  272. ctx.arc(m.x, m.y, m.radius, 0, 2 * Math.PI);
  273. ctx.stroke();
  274. if (m.status == 1) { // 选中状态,画个外框
  275. let rect = new Rect(m.x - m.radius, m.y - m.radius, m.x + m.radius, m.y + m.radius);
  276. ctx.save();
  277. ctx.strokeStyle = 'yellow';
  278. ctx.lineWidth = 2;
  279. ctx.setLineDash([4, 2]);
  280. ctx.strokeRect(rect.left, rect.top, rect.width(), rect.height());
  281. ctx.restore();
  282. }
  283. });
  284. ctx.restore();
  285. }
  286. /**
  287. * 绘制标记圈圈, 用于导出
  288. * @param ctx
  289. * @param scale 压缩比例
  290. */
  291. drawMarksOut(ctx: CanvasRenderingContext2D, scale = 1) {
  292. ctx.save();
  293. ctx.font = `25px sans-serif`;
  294. ctx.fillStyle = 'red';
  295. ctx.strokeStyle = '#00cc00';
  296. ctx.lineWidth = 8 / scale;
  297. this.innerMarks.forEach(m => {
  298. ctx.beginPath();
  299. ctx.arc(m.x / scale, m.y / scale, m.radius / scale, 0, 2 * Math.PI);
  300. let pos = this.getBestTextPos(ctx, m, scale);
  301. ctx.fillText(m.note, pos.x, pos.y);
  302. ctx.stroke();
  303. });
  304. ctx.restore();
  305. }
  306. // 获取最佳文字排版位置
  307. getBestTextPos(ctx: CanvasRenderingContext2D, mark: InnerMark, scale = 1): Point{
  308. let text = ctx.measureText(mark.note); // TextMetrics object
  309. let width = text.width;
  310. let x = Math.max(mark.x / scale - width / 2, 0);
  311. if (x + width > ctx.canvas.width) { // 文字超出了
  312. x = Math.max(ctx.canvas.width - width, 0);
  313. }
  314. let y = Math.min(mark.y / scale + 10, this.width / scale);
  315. return {x, y};
  316. }
  317. }
  318. export interface InnerMark {
  319. x: number;
  320. y: number;
  321. radius: number;
  322. note: string; // 文字说明
  323. status: number; //0-未选中,1-选中
  324. }