v1.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. const colors = ['#CFE7FE', '#DCEFF9', '#E0EBFC', '#DBE9FA', '#D4EEE9', '#E4DFF7', '#E7EBF6'];
  2. /**
  3. * 获取模型颜色。
  4. * Get model colors.
  5. *
  6. * @access public
  7. * @return array
  8. */
  9. function getModelColors()
  10. {
  11. const model = $(`#canvas_${model3cKey}`).closest('.model-canvas').data('model');
  12. let modelColors = colors;
  13. const colorGroup = {};
  14. if(colorGroup[`color${model}`] && Array.isArray(colorGroup[`color${model}`])) modelColors = colorGroup[`color${model}`];
  15. return modelColors;
  16. }
  17. /**
  18. * 初始模型画布。
  19. * Initialize the model canvas.
  20. *
  21. * @access public
  22. * @return void
  23. */
  24. window.initThinkCanvas = function()
  25. {
  26. const canvas = $(`#canvas_${model3cKey}`)[0];
  27. const ctx = canvas.getContext('2d');
  28. const image = new Image();
  29. const {model, mode} = $(`#canvas_${model3cKey}`).closest('.model-canvas').data();
  30. if(mode == 'view')
  31. {
  32. let $mainContent = $('#mainContent');
  33. $mainContent.addClass('load-indicator').addClass('loading').css('height', Math.max(200, Math.floor($(window).height() - $('#header').outerHeight() - $('#mainMenu').outerHeight()))).attr('data-loading', loadGraphTip);
  34. $('#main').removeClass('load-indicator');
  35. }
  36. image.src = `data/thinmory/thumbnail/init${model}.png`;
  37. image.onload = function()
  38. {
  39. const size = getCanvasSize(image.width, image.height);
  40. canvas.width = size.width;
  41. canvas.height = size.height;
  42. ctx.drawImage(image, 0, 0, size.width, size.height);
  43. const imageData = ctx.getImageData(0, 0, size.width, size.height);
  44. const pixels = binarizeImage(imageData);
  45. ctx.putImageData(pixels, 0, 0);
  46. const binarizeImageUrl = canvas.toDataURL();
  47. colorRegions(binarizeImageUrl).then((data) => {
  48. const coloredRegions = data.regions;
  49. const pixelData = data.pixelData;
  50. displayColoredImage(data.image);
  51. if(mode == 'preview')
  52. {
  53. renderRegionInput(coloredRegions);
  54. /* 点击画布外区域取消高亮。*/
  55. /* Click on the area outside the canvas to unhighlight.*/
  56. $(document).on('click', function(event)
  57. {
  58. const rect = canvas.getBoundingClientRect();
  59. const canvasX = rect.left;
  60. const canvasY = rect.top;
  61. const mouseX = event.clientX;
  62. const mouseY = event.clientY;
  63. /* 恢复整个画布的原始状态。 */
  64. ctx.putImageData(pixelData, 0, 0);
  65. /* 检测鼠标位置是否在特定区域内。 */
  66. /* Check if the mouse position is within a specific area. */
  67. if(canvasX > 0 && mouseX >= canvasX && mouseX <= (canvasX + canvas.width) && mouseY >= canvasY && mouseY <= (canvasY + canvas.height))
  68. {
  69. handleCanvasClick(mouseX, mouseY, canvas, ctx, coloredRegions);
  70. }
  71. });
  72. }
  73. else
  74. {
  75. for(var i = 0; i < coloredRegions.length - 1; i++)
  76. {
  77. if(coloredRegions[i + 1].minY - coloredRegions[i].minY < 8)
  78. {
  79. if(coloredRegions[i + 1].minX < coloredRegions[i].minX)
  80. {
  81. const item = coloredRegions[i];
  82. coloredRegions[i] = coloredRegions[i + 1];
  83. coloredRegions[i + 1] = item;
  84. }
  85. }
  86. }
  87. afterFinish();
  88. renderRegionTitles(coloredRegions);
  89. coloredRegions.forEach((region, index) => {
  90. calculateCardPosition(region, index);
  91. });
  92. }
  93. });
  94. };
  95. }
  96. function afterFinish()
  97. {
  98. setTimeout(function()
  99. {
  100. $('#mainContent').attr('data-loading', '').css('height', '').removeClass('loading').removeClass('load-indicator');
  101. $('#main').addClass('load-indicator');
  102. }, 200);
  103. };
  104. /**
  105. * 计算卡片位置。
  106. * Calculate card position.
  107. *
  108. * @param object region
  109. * @param number index
  110. * @access public
  111. * @return void
  112. */
  113. function calculateCardPosition(region, index)
  114. {
  115. const h = 180;
  116. const minW = 140;
  117. const positions = calculateCardPositionsOfScale(region);
  118. const cards = $(`#runResult .card.in_area-${index}`);
  119. if(cards.length <= positions.length)
  120. {
  121. cards.each((i, card) => {
  122. $(card).css({
  123. left: `${positions[i][0]}px`,
  124. top: `${positions[i][1]}px`,
  125. width: `${positions[i][2]}px`,
  126. height: `${h}px`,
  127. });
  128. });
  129. $('.card').removeClass('hidden');
  130. }
  131. else
  132. {
  133. const scale = 1 / Math.ceil(cards.length / positions.length);
  134. const newPositions = calculateCardPositionsOfScale(region, scale);
  135. cards.each((i, card) => {
  136. $(card).css({
  137. left: `${newPositions[i][0]}px`,
  138. top: `${newPositions[i][1]}px`,
  139. width: `${minW}px`,
  140. height: `${h}px`,
  141. transform: `scale(${scale}) translate(-50%, -50%)`,
  142. });
  143. });
  144. $('.card').removeClass('hidden');
  145. }
  146. }
  147. /**
  148. * 计算缩放的卡片位置。
  149. * Calculate the position of the scaled card.
  150. *
  151. * @param object region
  152. * @param number scale
  153. * @access public
  154. * @return void
  155. */
  156. function calculateCardPositionsOfScale(region, scale = 1)
  157. {
  158. let h = 180; // 原始卡片高度
  159. let minW = 140; // 原始卡片宽度
  160. let maxW = 280; // 最大宽度
  161. let d = 8; // 间距
  162. const rows = Object.values(region.rows);
  163. const rowsCount = rows.length;
  164. const positions = [];
  165. if(scale < 1)
  166. {
  167. h = h * scale;
  168. minW = minW * scale;
  169. maxW = maxW * scale;
  170. d = d * scale;
  171. }
  172. let r = 0;
  173. while (r < rowsCount)
  174. {
  175. let totalWidth = 0;
  176. let validStartRow = -1;
  177. for(let i = r; i < r + h && i < rowsCount; i++)
  178. {
  179. const rowsData = rows[i];
  180. let isValid = false;
  181. for(let j = 0; j < rowsData.length; j++)
  182. {
  183. const width = rowsData[j][1] - rowsData[j][0];
  184. if(width >= minW)
  185. {
  186. totalWidth += width;
  187. isValid = true;
  188. break;
  189. }
  190. }
  191. if(isValid) validStartRow = i;
  192. }
  193. if(validStartRow != -1)
  194. {
  195. let {left, right} = getMaxLeftAndMinRight(rows, validStartRow, h);
  196. if(scale == 1)
  197. {
  198. left += 4;
  199. right -= 28;
  200. }
  201. let rowN = Math.floor((right - left + d) / (minW + d));
  202. let w = (right - left + d) / rowN;
  203. for(let i = 0; i < rowN; i++)
  204. {
  205. let blockLeft = left + i * (w + d);
  206. let blockTop = rows[validStartRow][0][2];
  207. let blockWidth = Math.min(w, maxW);
  208. positions.push([blockLeft, blockTop, blockWidth]);
  209. }
  210. r += h;
  211. }
  212. else
  213. {
  214. r++;
  215. }
  216. }
  217. const groupedData = groupPositions(positions, h);
  218. return calcPositionsWithOffset(groupedData);
  219. }
  220. /**
  221. * 判断在同一行。
  222. * Judge on the same line.
  223. *
  224. * @param array currentItem
  225. * @param array nextItem
  226. * @param number offset
  227. * @access public
  228. * @return boolean
  229. */
  230. function isSameLine(currentItem, nextItem, offset)
  231. {
  232. return currentItem[1] == nextItem[1] || Math.abs(currentItem[1] - nextItem[1]) < offset;
  233. }
  234. /**
  235. * 将坐标分组。
  236. * Group positions.
  237. *
  238. * @param array data
  239. * @param number offset
  240. * @access public
  241. * @return array
  242. */
  243. function groupPositions(data, offset)
  244. {
  245. const groupedData = new Map();
  246. /* 分组数据 */
  247. /* Group data. */
  248. data.forEach(item => {
  249. const key = item[2];
  250. if (!groupedData.has(key)) groupedData.set(key, []);
  251. groupedData.get(key).push(item);
  252. });
  253. /* 合并临近的行 */
  254. /* Merge adjacent rows. */
  255. const mergedGroups = [];
  256. let currentGroup = null;
  257. let lastItem = null;
  258. for(let [key, items] of groupedData)
  259. {
  260. if(!currentGroup || !isSameLine(lastItem, items[items.length - 1], offset))
  261. {
  262. if(currentGroup) mergedGroups.push(currentGroup);
  263. currentGroup = {key, items};
  264. }
  265. else
  266. {
  267. currentGroup.items.push(...items);
  268. }
  269. lastItem = items[items.length - 1];
  270. }
  271. if(currentGroup) mergedGroups.push(currentGroup);
  272. return mergedGroups;
  273. }
  274. /**
  275. * 使用偏移量计算位置。
  276. * Calculate positions with offset.
  277. *
  278. * @param array groups
  279. * @access public
  280. * @return array
  281. */
  282. function calcPositionsWithOffset(groups)
  283. {
  284. const result = [];
  285. let yOffset = 0;
  286. groups.forEach(group => {
  287. const items = group.items;
  288. items.forEach(item => {
  289. result.push([item[0], item[1] + yOffset, item[2]]);
  290. });
  291. yOffset += 10;
  292. });
  293. return result;
  294. }
  295. /**
  296. * 获取最大左侧坐标和最小右侧坐标。
  297. * Get maxLeft and minRight.
  298. *
  299. * @param array rows
  300. * @param number start
  301. * @param number h
  302. * @access public
  303. * @return object
  304. */
  305. function getMaxLeftAndMinRight(rows, start, h)
  306. {
  307. const rowLength = rows[Math.min(start + h - 1, rows.length - 1)]?.length || 0;
  308. let left = rows[start][0][0] || 0;
  309. let right = rowLength ? rows[Math.min(start + h - 1, rows.length - 1)][rowLength - 1][1] : 0;
  310. for(let i = start; i < start + h; i++)
  311. {
  312. if(rows[i])
  313. {
  314. const rowData = rows[i];
  315. const maxLeft = rowData[0][0];
  316. const minRight = rowData[rowData.length - 1][1];
  317. left = Math.max(left, maxLeft);
  318. right = Math.min(right, minRight);
  319. }
  320. }
  321. return {left, right};
  322. }
  323. /**
  324. * 处理画布点击。
  325. * handle canvas click.
  326. *
  327. * @param object mouseX
  328. * @param object mouseY
  329. * @param object canvas
  330. * @param object ctx
  331. * @param array coloredRegions
  332. * @access public
  333. * @return void
  334. */
  335. function handleCanvasClick(mouseX, mouseY, canvas, ctx, coloredRegions)
  336. {
  337. const rect = canvas.getBoundingClientRect();
  338. const x = mouseX - rect.left;
  339. const y = mouseY - rect.top;
  340. coloredRegions.forEach((region, index) => {
  341. let isInRegion = false;
  342. region.rows.forEach((row, key) => {
  343. if(parseInt(y) === key && row) isInRegion = row.some((item) => (x >= item[0] && x <= item[1]));
  344. });
  345. if(isInRegion)
  346. {
  347. if($(`#input_${model3cKey}_${index}`).length) $(`#input_${model3cKey}_${index}`)[0].focus();
  348. renderHighlightedRegion(index, coloredRegions[index], ctx);
  349. }
  350. });
  351. }
  352. $(document).on('blur', '.model-canvas input', function()
  353. {
  354. $(this).attr('title', $(this).val());
  355. });
  356. /**
  357. * 渲染高亮的区域。
  358. * Render highlighted region.
  359. *
  360. * @param number index
  361. * @param object region
  362. * @param object ctx
  363. * @access public
  364. * @return void
  365. */
  366. function renderHighlightedRegion(index, region, ctx)
  367. {
  368. const color = colors[index % colors.length];
  369. let regionPath = new Path2D();
  370. region.rows.forEach((row, top) => {
  371. row.forEach((item) => {
  372. regionPath.rect(item[0], item[2], item[1] - item[0] + 1, 1);
  373. });
  374. });
  375. ctx.fillStyle = color;
  376. ctx.fill(regionPath);
  377. }
  378. /**
  379. * 将图像二值化。
  380. * Binarize the image.
  381. *
  382. * @param object imageData
  383. * @access public
  384. * @return object
  385. */
  386. function binarizeImage(imageData)
  387. {
  388. const threshold = 200;
  389. let pixels = imageData.data;
  390. for(let i = 0; i < pixels.length; i += 4)
  391. {
  392. const brightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
  393. const color = brightness > threshold ? 255 : 0; // 255 为白色,0 为黑色
  394. pixels[i] = pixels[i + 1] = pixels[i + 2] = color;
  395. }
  396. imageData.data = pixels;
  397. return imageData;
  398. }
  399. /**
  400. * 获取画布尺寸。
  401. * Get canvas size.
  402. *
  403. * @param number width
  404. * @param number height
  405. * @access public
  406. * @return object
  407. */
  408. function getCanvasSize(width = 0, height = 0)
  409. {
  410. if(model3cKey == 'link') return {width: 420, height: 400};
  411. const mode = $(`#canvas_${model3cKey}`).closest('.model-canvas').data('mode');
  412. if(mode == 'view') return {width: 2400, height: 2400};
  413. const maxWidth = $('.think-model-content').length ? ($('.think-model-content').width() - 50) : 400;
  414. const maxHeight = maxWidth;
  415. if(width > maxWidth || height > maxHeight)
  416. {
  417. if(width > height)
  418. {
  419. height *= maxWidth / width;
  420. width = maxWidth;
  421. }
  422. else
  423. {
  424. width *= maxHeight / height;
  425. height = maxHeight;
  426. }
  427. }
  428. if(!width)
  429. {
  430. width = maxWidth;
  431. height = maxHeight;
  432. }
  433. return {width, height};
  434. }
  435. /**
  436. * 获取区域信息。
  437. * Get region info.
  438. *
  439. * @param object pixels
  440. * @param boolean visited
  441. * @param number x
  442. * @param number y
  443. * @param number width
  444. * @param number height
  445. * @access public
  446. * @return object
  447. */
  448. function getRegionInfo(pixels, visited, x, y, width, height)
  449. {
  450. const region = {area: 0, rows: [], minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity};
  451. /* 创建栈数据结构。*/
  452. /* Create a stack data structure.*/
  453. const stack = [[x, y]];
  454. while(stack.length > 0)
  455. {
  456. const [currentX, currentY] = stack.pop();
  457. const index = (currentY * width * 4) + (currentX * 4);
  458. if(pixels[index] === 255 && !visited[currentY + ',' + currentX])
  459. {
  460. visited[currentY + ',' + currentX] = true;
  461. region.area++;
  462. /* 将rows[currentY] 设为一个数组,以便存储多个区域段, 格式为:[left, right, top]。*/
  463. /* Set rows [CurrentY] as an array to store multiple regional segments. */
  464. if (!region.rows[currentY]) region.rows[currentY] = [];
  465. region.rows[currentY].push([Infinity, -Infinity, currentY]);
  466. let currentRow = region.rows[currentY][region.rows[currentY].length - 1];
  467. currentRow[0] = Math.min(currentRow[0], currentX);
  468. currentRow[1] = Math.max(currentRow[1], currentX);
  469. region.minX = Math.min(region.minX, currentX);
  470. region.minY = Math.min(region.minY, currentY);
  471. region.maxX = Math.max(region.maxX, currentX);
  472. region.maxY = Math.max(region.maxY, currentY);
  473. if (currentX > 0) stack.push([currentX - 1, currentY]);
  474. if (currentX < width - 1) stack.push([currentX + 1, currentY]);
  475. if (currentY > 0) stack.push([currentX, currentY - 1]);
  476. if (currentY < height - 1) stack.push([currentX, currentY + 1]);
  477. }
  478. }
  479. return region;
  480. }
  481. /**
  482. * 查找区域。
  483. * Find regions.
  484. *
  485. * @param object pixels
  486. * @param number width
  487. * @param number height
  488. * @access public
  489. * @return array
  490. */
  491. function findRegions(pixels, width, height)
  492. {
  493. const visited = {};
  494. const regions = [];
  495. const mode = $(`#canvas_${model3cKey}`).closest('.model-canvas').data('mode');
  496. const minArea = mode == 'view' ? 600 : 100;
  497. for(let y = 0; y < height; y++)
  498. {
  499. for(let x = 0; x < width; x++)
  500. {
  501. const index = (y * width * 4) + (x * 4);
  502. if(pixels[index] === 255 && !visited[y + ',' + x])
  503. {
  504. const region = getRegionInfo(pixels, visited, x, y, width, height);
  505. /* 过滤掉小区域。 */
  506. /* Filter out small regions. */
  507. if(region.area > minArea)
  508. {
  509. region.rows.forEach((row, key) => {
  510. region.rows[key] = processRegionRows(row);
  511. });
  512. regions.push(region);
  513. }
  514. }
  515. }
  516. }
  517. return regions;
  518. }
  519. /**
  520. * 多维数据扁平化。
  521. * Flatten data.
  522. *
  523. * @param array data
  524. * @access public
  525. * @return array
  526. */
  527. function flattenData(data)
  528. {
  529. const result = [];
  530. const stack = data.slice();
  531. while (stack.length > 0)
  532. {
  533. const item = stack.pop();
  534. if(typeof item === 'object' && item !== null && Object.prototype.toString.call(item) === '[object Array]')
  535. {
  536. for(let i = item.length - 1; i >= 0; i--)
  537. {
  538. stack.push(item[i]);
  539. }
  540. }
  541. else
  542. {
  543. result.push(item);
  544. }
  545. }
  546. return result;
  547. }
  548. /**
  549. * 处理区域行数据。
  550. * Process region rows data.
  551. *
  552. * @param array data
  553. * @access public
  554. * @return array
  555. */
  556. function processRegionRows(data)
  557. {
  558. if(!data?.length) return [];
  559. /* 按照 left 的大小对数组进行升序排序。 */
  560. /* Sort the array in ascending order according to the size of left. */
  561. data.sort((a, b) => a[0] - b[0]);
  562. let result = [];
  563. let currentItem = [data[0][0], data[0][1], data[0][2]];
  564. for(let i = 1; i < data.length; i++)
  565. {
  566. if(data[i][0] === currentItem[1] + 1 && data[i][2] === currentItem[2])
  567. {
  568. currentItem[1] = data[i][1];
  569. }
  570. else if(data[i][1] === currentItem[0] - 1 && data[i][2] === currentItem[2])
  571. {
  572. currentItem[0] = data[i][0];
  573. }
  574. else
  575. {
  576. result.push([currentItem[0], currentItem[1], currentItem[2]]);
  577. currentItem = [data[i][0], data[i][1], data[i][2]];
  578. }
  579. }
  580. result.push([currentItem[0], currentItem[1], currentItem[2]]);
  581. return result;
  582. }
  583. /**
  584. * 绘制区域输入框。
  585. * Draw a region input.
  586. *
  587. * @param object canvas
  588. * @param object region
  589. * @param number colorIndex
  590. * @param string clientLang
  591. * @param array blocks
  592. * @access public
  593. * @return void
  594. */
  595. window.drawRegionInput = function(canvas, region, colorIndex, clientLang, blocks)
  596. {
  597. region.minX += 30;
  598. region.maxY -= 10;
  599. const centerX = (region.minX + region.maxX) / 2;
  600. const centerY = (region.minY + region.maxY) / 2;
  601. const boxWidth = 60;
  602. const boxHeight = 32;
  603. const disabled = $(`#canvas_${model3cKey}`).closest('.model-canvas').data('disabled');
  604. let left = centerX - boxWidth / 2;
  605. let top = centerY - boxHeight / 2;
  606. if(colorIndex == 1) left -= 20;
  607. if(colorIndex == 2) left += 30;
  608. if(['zh-cn', 'zh-tw'].includes(clientLang))
  609. {
  610. if([0, 4, 5].includes(colorIndex)) top -= 40;
  611. if(colorIndex == 4) left -= 50;
  612. if(colorIndex == 5) left += 60;
  613. }
  614. else
  615. {
  616. if([0, 4, 5].includes(colorIndex))
  617. {
  618. top -= 30;
  619. if(colorIndex == 4) left += $(window).width() > 1366 ? -50 : 50;
  620. if(colorIndex == 5) left += $(window).width() > 1366 ? 60 : -60;
  621. }
  622. }
  623. const styles = {
  624. position: 'absolute',
  625. left: `${left}px`,
  626. top: `${top}px`,
  627. width: `${boxWidth}px`,
  628. height: `${boxHeight}px`,
  629. padding: '5px',
  630. fontSize: '16px',
  631. color: '#000',
  632. backgroundColor: 'transparent',
  633. };
  634. /* 按照索引顺序设置area属性为a~z。 */
  635. /* Set the area attribute to a~z in index order. */
  636. const block = String.fromCharCode(97 + colorIndex);
  637. const input = document.createElement('input');
  638. const value = blocks[colorIndex] || block;
  639. input.type = 'text';
  640. input.value = value;
  641. input.title = value;
  642. input.id = `input_${model3cKey}_${colorIndex}`;
  643. input.name = `blocks[]`;
  644. input.placeholder = blockName;
  645. input.disabled = disabled;
  646. input.setAttribute('data-index', colorIndex);
  647. input.setAttribute('data-block', value);
  648. Object.assign(input.style, styles);
  649. canvas.parentNode.appendChild(input);
  650. }
  651. /**
  652. * 绘制区域标题。
  653. * Draw a region title.
  654. *
  655. * @param object canvas
  656. * @param object region
  657. * @param number colorIndex
  658. * @param array blocks
  659. * @access public
  660. * @return void
  661. */
  662. window.drawRegionTitle = function(canvas, region, colorIndex, blocks)
  663. {
  664. const centerX = (region.minX + region.maxX) / 2;
  665. const centerY = (region.minY + region.maxY) / 2;
  666. const boxWidth = 120;
  667. const boxHeight = 48;
  668. let left = centerX - boxWidth / 2;
  669. let top = centerY - boxHeight / 2;
  670. let bottom = 0;
  671. const positionMap = {
  672. 0: {left: left, top: 40},
  673. 1: {left: left - 60, top: top + 300},
  674. 2: {left: left + 70, top: top + 300},
  675. 3: {left: left, top: top - 360},
  676. 4: {left: left - 260, top: top - 650},
  677. 5: {left: left + 260, top: top - 650},
  678. 6: {left: left, top: 0, bottom: 160}
  679. };
  680. if(colorIndex in positionMap)
  681. {
  682. const {left: newLeft, top: newTop, bottom: newBottom = 'unset'} = positionMap[colorIndex];
  683. left = newLeft;
  684. top = newTop;
  685. bottom = newBottom;
  686. }
  687. const styles = {
  688. left: left ? `${left}px` : 'unset',
  689. top: top ? `${top}px` : 'unset',
  690. bottom: bottom ? `${bottom}px` : 'unset'
  691. };
  692. const block = String.fromCharCode(97 + colorIndex);
  693. const $title = document.createElement('div');
  694. const value = blocks[colorIndex] ? blocks[colorIndex]?.text : block;
  695. $title.innerHTML = value;
  696. $title.title = value;
  697. $title.id = `title_${colorIndex}`;
  698. $title.className = 'w-28 p-1 item-step-title text-clip text-center absolute';
  699. Object.assign($title.style, styles);
  700. canvas.parentNode.appendChild($title);
  701. }
  702. /**
  703. * 渲染区域标题。
  704. * Render region titles.
  705. *
  706. * @param array regions
  707. * @access public
  708. * @return void
  709. */
  710. function renderRegionTitles(regions)
  711. {
  712. const {model, blocks} = $(`#canvas_${model3cKey}`).closest('.model-canvas').data();
  713. let drawTitleFunc = `drawRegionTitleOf${model}`;
  714. drawTitleFunc = (drawTitleFunc in window) ? window[drawTitleFunc] : window['drawRegionTitle'];
  715. const canvas = $(`#canvas_${model3cKey}`)[0];
  716. regions.forEach((region, index) => {
  717. if(!$(`#title_${index}`).length) drawTitleFunc(canvas, region, index, blocks);
  718. });
  719. }
  720. /**
  721. * 渲染区域输入框。
  722. * Render region inputs.
  723. *
  724. * @param array regions
  725. * @access public
  726. * @return void
  727. */
  728. function renderRegionInput(regions)
  729. {
  730. const {model, blocks, clientLang} = $(`#canvas_${model3cKey}`).closest('.model-canvas').data();
  731. let drawInputFunc = `drawRegionInputOf${model}`;
  732. drawInputFunc = (drawInputFunc in window) ? window[drawInputFunc] : window['drawRegionInput'];
  733. const canvas = $(`#canvas_${model3cKey}`)[0];
  734. regions.forEach((region, index) => {
  735. if(!$(`#input_${model3cKey}_${index}`).length) drawInputFunc(canvas, region, index, clientLang, blocks);
  736. });
  737. }
  738. /**
  739. * 图像着色。
  740. * Color the image.
  741. *
  742. * @param string binaryImageData
  743. * @access public
  744. * @return object
  745. */
  746. function colorRegions(binaryImageData)
  747. {
  748. const canvas = document.createElement('canvas');
  749. const ctx = canvas.getContext('2d');
  750. const img = new Image();
  751. img.src = binaryImageData;
  752. return new Promise((resolve, reject) => {
  753. img.onload = function()
  754. {
  755. canvas.width = img.width;
  756. canvas.height = img.height;
  757. ctx.drawImage(img, 0, 0);
  758. const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  759. let regions = findRegions(pixels, canvas.width, canvas.height);
  760. ctx.globalCompositeOperation = 'source-over';
  761. ctx.globalAlpha = 0.5;
  762. regions.forEach((region, colorIndex) => {
  763. /* 遍历区域所有行,进行颜色填充。 */
  764. /* Traverse all rows in the region and fill them with colors. */
  765. let regionPath = new Path2D();
  766. region.rows.forEach((row, key) => {
  767. row.forEach((item) => {
  768. regionPath.rect(item[0], key, item[1] - item[0] + 1, 1);
  769. });
  770. });
  771. ctx.fillStyle = colors[colorIndex % colors.length];
  772. ctx.fill(regionPath);
  773. });
  774. const originalPixelData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  775. originalPixelData.willReadFrequently = true;
  776. resolve({image: canvas.toDataURL(), regions: regions, pixelData: originalPixelData});
  777. };
  778. });
  779. }
  780. /**
  781. * 展示着色的图像。
  782. * Display colored image.
  783. *
  784. * @param string coloredImageData
  785. * @access public
  786. * @return void
  787. */
  788. function displayColoredImage(coloredImageData)
  789. {
  790. const canvas = $(`#canvas_${model3cKey}`)[0];
  791. const ctx = canvas.getContext('2d');
  792. const img = new Image();
  793. img.src = coloredImageData;
  794. img.onload = function(){ctx.drawImage(img, 0, 0);};
  795. }