point.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>YOLO图片标注工具</title>
  6. <style>
  7. body { font-family: Arial, sans-serif; margin: 0; background: #f7f7f7; }
  8. #main-container {
  9. display: flex;
  10. height: 100vh;
  11. }
  12. #left-panel {
  13. width: 220px;
  14. background: #fff;
  15. border-right: 1px solid #eee;
  16. padding: 20px 10px 10px 10px;
  17. box-sizing: border-box;
  18. display: flex;
  19. flex-direction: column;
  20. }
  21. #imgList {
  22. flex: 1;
  23. overflow-y: auto;
  24. margin-top: 10px;
  25. }
  26. .img-url-item {
  27. padding: 6px 8px;
  28. border-radius: 4px;
  29. cursor: pointer;
  30. margin-bottom: 4px;
  31. transition: background 0.2s;
  32. }
  33. .img-url-item.active, .img-url-item:hover {
  34. background: #e6f7ff;
  35. }
  36. #center-panel {
  37. flex: 1;
  38. display: flex;
  39. flex-direction: column;
  40. align-items: center;
  41. justify-content: flex-start;
  42. padding: 20px;
  43. max-width: 900px;
  44. max-height: 600px;
  45. box-sizing: border-box;
  46. }
  47. #canvas {
  48. border: 1px solid #ccc;
  49. cursor: crosshair;
  50. background: #fff;
  51. box-shadow: 0 2px 8px #eee;
  52. max-width: 900px;
  53. max-height: 600px;
  54. width: 100%;
  55. height: auto;
  56. }
  57. #controls {
  58. margin-bottom: 10px;
  59. width: 100%;
  60. display: flex;
  61. gap: 8px;
  62. justify-content: center;
  63. }
  64. #right-panel {
  65. width: 260px;
  66. background: #fff;
  67. border-left: 1px solid #eee;
  68. padding: 20px 10px 10px 10px;
  69. box-sizing: border-box;
  70. display: flex;
  71. flex-direction: column;
  72. }
  73. #boxes-list {
  74. margin-top: 10px;
  75. flex: 1;
  76. overflow-y: auto;
  77. }
  78. .box-item {
  79. font-size: 14px;
  80. background: #f5f5f5;
  81. margin-bottom: 6px;
  82. padding: 6px 8px;
  83. border-radius: 4px;
  84. display: flex;
  85. justify-content: space-between;
  86. align-items: center;
  87. }
  88. .del-btn {
  89. background: #ff7875;
  90. color: #fff;
  91. border: none;
  92. border-radius: 3px;
  93. padding: 2px 8px;
  94. cursor: pointer;
  95. font-size: 12px;
  96. }
  97. .del-btn:hover {
  98. background: #d9363e;
  99. }
  100. #exportBtn {
  101. margin-top: 10px;
  102. width: 100%;
  103. background: #1890ff;
  104. color: #fff;
  105. border: none;
  106. border-radius: 4px;
  107. padding: 8px 0;
  108. font-size: 15px;
  109. cursor: pointer;
  110. }
  111. #exportBtn:disabled {
  112. background: #ccc;
  113. color: #fff;
  114. cursor: not-allowed;
  115. }
  116. h2 { margin: 0 0 18px 0; text-align: center; }
  117. #imgUrlInput { width: 100%; box-sizing: border-box; }
  118. #loadImgBtn { width: 100%; }
  119. .img-thumb {
  120. width: 100%;
  121. max-width: 180px;
  122. height: 90px;
  123. object-fit: cover;
  124. border-radius: 6px;
  125. border: 2px solid transparent;
  126. margin-bottom: 8px;
  127. cursor: pointer;
  128. transition: border 0.2s;
  129. background: #f0f0f0;
  130. }
  131. .img-thumb.active {
  132. border: 2px solid #1890ff;
  133. }
  134. </style>
  135. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  136. </head>
  137. <body>
  138. <div id="main-container">
  139. <!-- 左侧图片列表 -->
  140. <div id="left-panel">
  141. <h2>图片列表</h2>
  142. <button id="clearBoxesBtn" style="width:100%;margin-bottom:10px;background:#ff7875;color:#fff;border:none;border-radius:4px;padding:8px 0;font-size:15px;cursor:pointer;">清空所有标记</button>
  143. <div id="imgList"></div>
  144. </div>
  145. <!-- 中间标注区 -->
  146. <div id="center-panel">
  147. <h2>图片标注区</h2>
  148. <div id="zoom-controls" style="margin-bottom:10px;display:flex;gap:8px;justify-content:center;">
  149. <button id="zoomInBtn" style="padding:4px 14px;">放大</button>
  150. <button id="zoomOutBtn" style="padding:4px 14px;">缩小</button>
  151. <button id="zoomResetBtn" style="padding:4px 14px;">重置</button>
  152. </div>
  153. <canvas id="canvas"></canvas>
  154. </div>
  155. <!-- 右侧标注结果 -->
  156. <div id="right-panel">
  157. <h2>标注结果</h2>
  158. <div id="boxes-list"></div>
  159. <button id="exportBtn" disabled>导出YOLO标注</button>
  160. </div>
  161. </div>
  162. <script>
  163. let img = new window.Image();
  164. let boxes = [];
  165. let drawing = false;
  166. let startX = 0, startY = 0, endX = 0, endY = 0;
  167. let imgWidth = 0, imgHeight = 0;
  168. let selectedBoxIndex = -1; // 当前选中的框索引
  169. let dragMode = null; // 拖动模式:null/move/resize
  170. let dragOffsetX = 0, dragOffsetY = 0; // 拖动时鼠标与框左上角的偏移
  171. const HANDLE_SIZE = 10; // 手柄的像素大小
  172. // 8个手柄类型,分别对应四个角和四条边中点
  173. const HANDLE_TYPES = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
  174. // 缩放相关
  175. let scale = 1.0;
  176. let minScale = 0.2, maxScale = 5.0;
  177. let offsetX = 0, offsetY = 0; // 平移偏移(可扩展拖动画布)
  178. let isPanning = false; // 是否正在拖动画布
  179. let panStart = {x: 0, y: 0}; // 拖拽起点
  180. let panOffsetStart = {x: 0, y: 0}; // 拖拽时的初始偏移
  181. let spacePressed = false; // 是否按下空格
  182. // 判断点(x, y)是否在框box内部
  183. function isInBox(x, y, box) {
  184. return x >= box.x && x <= box.x + box.w && y >= box.y && y <= box.y + box.h;
  185. }
  186. // 获取8个手柄的中心坐标
  187. function getHandles(box, size) {
  188. const x = box.x, y = box.y, w = box.w, h = box.h;
  189. const hs = (size || HANDLE_SIZE) / 2;
  190. return {
  191. nw: { x: x - hs, y: y - hs }, // 左上角
  192. n: { x: x + w/2 - hs, y: y - hs }, // 上中
  193. ne: { x: x + w - hs, y: y - hs }, // 右上角
  194. e: { x: x + w - hs, y: y + h/2 - hs }, // 右中
  195. se: { x: x + w - hs, y: y + h - hs }, // 右下角
  196. s: { x: x + w/2 - hs, y: y + h - hs }, // 下中
  197. sw: { x: x - hs, y: y + h - hs }, // 左下角
  198. w: { x: x - hs, y: y + h/2 - hs } // 左中
  199. };
  200. }
  201. // 判断点(x, y)是否在某个手柄上,返回手柄类型
  202. function getHandleAt(x, y, box) {
  203. let domW = $('#canvas').width();
  204. let pixelRatio = $('#canvas')[0].width / domW;
  205. let handleSize = 10 * pixelRatio;
  206. const handles = getHandles(box, handleSize);
  207. for (let type of HANDLE_TYPES) {
  208. const hx = handles[type].x, hy = handles[type].y;
  209. if (x >= hx && x <= hx + handleSize && y >= hy && y <= hy + handleSize) {
  210. return type;
  211. }
  212. }
  213. return null;
  214. }
  215. // 根据手柄类型设置鼠标指针样式
  216. function setCursorByHandle(type) {
  217. const map = {
  218. nw: 'nwse-resize',
  219. se: 'nwse-resize',
  220. ne: 'nesw-resize',
  221. sw: 'nesw-resize',
  222. n: 'ns-resize',
  223. s: 'ns-resize',
  224. e: 'ew-resize',
  225. w: 'ew-resize'
  226. };
  227. $('#canvas').css('cursor', map[type] || 'default');
  228. }
  229. // 绘制函数,支持缩放和平移
  230. function drawAll() {
  231. const ctx = $('#canvas')[0].getContext('2d');
  232. ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  233. ctx.save();
  234. ctx.scale(scale, scale);
  235. ctx.translate(offsetX, offsetY);
  236. ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
  237. // 计算canvas显示缩放比例
  238. let domW = $('#canvas').width();
  239. let pixelRatio = $('#canvas')[0].width / domW;
  240. let lineWidth = 2 * pixelRatio / scale; // 屏幕上2px
  241. let handleSize = 10 * pixelRatio / scale; // 屏幕上10px
  242. boxes.forEach((box, idx) => {
  243. ctx.save();
  244. if (idx === selectedBoxIndex) {
  245. ctx.strokeStyle = 'orange'; // 选中框高亮
  246. ctx.lineWidth = lineWidth;
  247. } else {
  248. ctx.strokeStyle = 'red';
  249. ctx.lineWidth = lineWidth;
  250. }
  251. ctx.strokeRect(box.x, box.y, box.w, box.h);
  252. // 绘制8个缩放手柄
  253. if (idx === selectedBoxIndex) {
  254. ctx.fillStyle = 'blue';
  255. const handles = getHandles(box, handleSize);
  256. for (let type of HANDLE_TYPES) {
  257. ctx.fillRect(handles[type].x, handles[type].y, handleSize, handleSize);
  258. }
  259. }
  260. ctx.restore();
  261. });
  262. // 绘制新建时的临时框
  263. if (drawing) {
  264. ctx.strokeStyle = 'blue';
  265. ctx.lineWidth = lineWidth;
  266. ctx.strokeRect(
  267. Math.min(startX, endX),
  268. Math.min(startY, endY),
  269. Math.abs(endX - startX),
  270. Math.abs(endY - startY)
  271. );
  272. }
  273. ctx.restore();
  274. }
  275. function updateBoxesList() {
  276. let html = '';
  277. boxes.forEach((box, idx) => {
  278. html += `<div class="box-item">框${idx+1} [x:${box.x}, y:${box.y}, w:${box.w}, h:${box.h}] <button data-idx="${idx}" class="del-btn">删除</button></div>`;
  279. });
  280. $('#boxes-list').html(html);
  281. $('#exportBtn').prop('disabled', boxes.length === 0);
  282. }
  283. // 工具函数:canvas坐标转原图坐标
  284. function toImageCoord(canvasX, canvasY) {
  285. // 需要考虑canvas缩放后的显示尺寸
  286. let domW = $('#canvas').width();
  287. let domH = $('#canvas').height();
  288. let ratioX = imgWidth / domW;
  289. let ratioY = imgHeight / domH;
  290. return {
  291. x: (canvasX * ratioX / scale - offsetX),
  292. y: (canvasY * ratioY / scale - offsetY)
  293. };
  294. }
  295. $(function() {
  296. // 图片URL缩略图列表,默认一张图片
  297. let imgUrlArr = [
  298. 'https://image.cszcyl.cn/2022/image/YfJA8eON3exqppNSQrW5EhE9rGdNS1qwou5dgj3L3651WcDDZEy89Hn1A296TXIx.png',
  299. 'https://image.cszcyl.cn/coze/%E9%AB%98%E8%80%83%E5%BF%85%E8%83%9C-%E8%B6%85%E6%B8%85.png',
  300. 'https://image.cszcyl.cn/2022/image/1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3.png',
  301. 'https://image.cszcyl.cn/2022/image/abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567abc890def123.png',
  302. 'https://image.cszcyl.cn/2022/image/sample2.png'
  303. ];
  304. let currentImgIndex = 0;
  305. // 渲染左侧图片缩略图列表
  306. function renderImgList() {
  307. let html = '';
  308. imgUrlArr.forEach((url, idx) => {
  309. html += `<img src="${url}" class="img-thumb${idx===currentImgIndex?' active':''}" data-idx="${idx}" title="${url}">`;
  310. });
  311. $('#imgList').html(html);
  312. }
  313. renderImgList();
  314. // 点击图片缩略图切换图片
  315. $('#imgList').on('click', '.img-thumb', function() {
  316. const idx = $(this).data('idx');
  317. if (currentImgIndex === idx) return;
  318. currentImgIndex = idx;
  319. renderImgList();
  320. loadCurrentImage();
  321. });
  322. // 加载当前选中图片到canvas
  323. function loadCurrentImage() {
  324. const url = imgUrlArr[currentImgIndex];
  325. img.onload = function() {
  326. imgWidth = img.width;
  327. imgHeight = img.height;
  328. // 计算缩放比例,canvas最大900x600
  329. let maxW = 900, maxH = 600;
  330. let ratio = Math.min(1, maxW / imgWidth, maxH / imgHeight);
  331. let showW = Math.round(imgWidth * ratio);
  332. let showH = Math.round(imgHeight * ratio);
  333. $('#canvas').attr({ width: imgWidth, height: imgHeight }); // 逻辑像素
  334. $('#canvas').css({ width: showW + 'px', height: showH + 'px' }); // 显示像素
  335. scale = 1.0;
  336. offsetX = 0;
  337. offsetY = 0;
  338. boxes = [];
  339. drawAll();
  340. updateBoxesList();
  341. };
  342. img.onerror = function() {
  343. alert('图片加载失败,请检查URL是否正确');
  344. };
  345. img.src = url;
  346. }
  347. // 页面加载时自动加载第一张图片
  348. loadCurrentImage();
  349. let resizeHandle = null; // 当前正在拖动的手柄类型
  350. // 监听键盘空格按下/松开
  351. $(document).on('keydown', function(e) {
  352. if (e.code === 'Space') spacePressed = true;
  353. });
  354. $(document).on('keyup', function(e) {
  355. if (e.code === 'Space') spacePressed = false;
  356. });
  357. // 鼠标按下事件
  358. $('#canvas').on('mousedown', function(e) {
  359. if (!imgWidth) return;
  360. const rect = this.getBoundingClientRect();
  361. const canvasX = e.clientX - rect.left;
  362. const canvasY = e.clientY - rect.top;
  363. const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
  364. // 新增:空格+左键 或 右键拖动画布
  365. if ((spacePressed && e.button === 0) || e.button === 2) {
  366. isPanning = true;
  367. panStart = {x: e.clientX, y: e.clientY};
  368. panOffsetStart = {x: offsetX, y: offsetY};
  369. $('#canvas').css('cursor', 'grab');
  370. return;
  371. }
  372. let found = false;
  373. resizeHandle = null;
  374. // 优先判断是否点中手柄
  375. for (let i = boxes.length - 1; i >= 0; i--) {
  376. const handleType = getHandleAt(mouseX, mouseY, boxes[i]);
  377. if (handleType) {
  378. selectedBoxIndex = i;
  379. dragMode = 'resize';
  380. resizeHandle = handleType;
  381. found = true;
  382. break;
  383. } else if (isInBox(mouseX, mouseY, boxes[i])) {
  384. // 点中框内部,准备移动
  385. selectedBoxIndex = i;
  386. dragMode = 'move';
  387. dragOffsetX = mouseX - boxes[i].x;
  388. dragOffsetY = mouseY - boxes[i].y;
  389. found = true;
  390. break;
  391. }
  392. }
  393. if (found) {
  394. drawing = false;
  395. drawAll();
  396. return;
  397. }
  398. // 没点中任何框,开始新建
  399. selectedBoxIndex = -1;
  400. drawing = true;
  401. startX = endX = mouseX;
  402. startY = endY = mouseY;
  403. drawAll();
  404. });
  405. // 鼠标移动事件
  406. $('#canvas').on('mousemove', function(e) {
  407. const rect = this.getBoundingClientRect();
  408. const canvasX = e.clientX - rect.left;
  409. const canvasY = e.clientY - rect.top;
  410. const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
  411. // 新增:画布拖拽
  412. if (isPanning) {
  413. let domW = $('#canvas').width();
  414. let domH = $('#canvas').height();
  415. let canvasW = $('#canvas')[0].width;
  416. let canvasH = $('#canvas')[0].height;
  417. let ratioX = canvasW / domW;
  418. let ratioY = canvasH / domH;
  419. let dx = (e.clientX - panStart.x) * ratioX / scale;
  420. let dy = (e.clientY - panStart.y) * ratioY / scale;
  421. offsetX = panOffsetStart.x + dx;
  422. offsetY = panOffsetStart.y + dy;
  423. drawAll();
  424. return;
  425. }
  426. if (drawing) {
  427. // 新建框时,动态绘制,限制不出界
  428. endX = Math.max(0, Math.min(mouseX, imgWidth));
  429. endY = Math.max(0, Math.min(mouseY, imgHeight));
  430. drawAll();
  431. } else if (selectedBoxIndex !== -1 && dragMode) {
  432. let box = boxes[selectedBoxIndex];
  433. let minW = 10, minH = 10;
  434. let x = box.x, y = box.y, w = box.w, h = box.h;
  435. if (dragMode === 'move') {
  436. // 拖动移动框
  437. let newX = mouseX - dragOffsetX;
  438. let newY = mouseY - dragOffsetY;
  439. newX = Math.max(0, Math.min(newX, imgWidth - w));
  440. newY = Math.max(0, Math.min(newY, imgHeight - h));
  441. box.x = newX;
  442. box.y = newY;
  443. } else if (dragMode === 'resize' && resizeHandle) {
  444. // 8方向缩放
  445. switch (resizeHandle) {
  446. case 'nw': // 左上角
  447. box.x = Math.min(mouseX, x + w - minW);
  448. box.y = Math.min(mouseY, y + h - minH);
  449. box.w = w + (x - box.x);
  450. box.h = h + (y - box.y);
  451. break;
  452. case 'n': // 上中
  453. box.y = Math.min(mouseY, y + h - minH);
  454. box.h = h + (y - box.y);
  455. break;
  456. case 'ne': // 右上角
  457. box.y = Math.min(mouseY, y + h - minH);
  458. box.w = Math.max(minW, mouseX - x);
  459. box.h = h + (y - box.y);
  460. break;
  461. case 'e': // 右中
  462. box.w = Math.max(minW, mouseX - x);
  463. break;
  464. case 'se': // 右下角
  465. box.w = Math.max(minW, mouseX - x);
  466. box.h = Math.max(minH, mouseY - y);
  467. break;
  468. case 's': // 下中
  469. box.h = Math.max(minH, mouseY - y);
  470. break;
  471. case 'sw': // 左下角
  472. box.x = Math.min(mouseX, x + w - minW);
  473. box.w = w + (x - box.x);
  474. box.h = Math.max(minH, mouseY - y);
  475. break;
  476. case 'w': // 左中
  477. box.x = Math.min(mouseX, x + w - minW);
  478. box.w = w + (x - box.x);
  479. break;
  480. }
  481. // 限制不出界
  482. if (box.x < 0) { box.w += box.x; box.x = 0; }
  483. if (box.y < 0) { box.h += box.y; box.y = 0; }
  484. if (box.x + box.w > imgWidth) box.w = imgWidth - box.x;
  485. if (box.y + box.h > imgHeight) box.h = imgHeight - box.y;
  486. }
  487. drawAll();
  488. updateBoxesList();
  489. } else if (selectedBoxIndex !== -1) {
  490. // 悬停手柄时改变鼠标样式
  491. const box = boxes[selectedBoxIndex];
  492. const handleType = getHandleAt(mouseX, mouseY, box);
  493. setCursorByHandle(handleType);
  494. } else {
  495. $('#canvas').css('cursor', 'crosshair');
  496. }
  497. });
  498. // 鼠标松开事件
  499. $(document).on('mouseup', function(e) {
  500. if (isPanning) {
  501. isPanning = false;
  502. $('#canvas').css('cursor', spacePressed ? 'grab' : 'crosshair');
  503. return;
  504. }
  505. const rect = $('#canvas')[0].getBoundingClientRect();
  506. const canvasX = e.clientX - rect.left;
  507. const canvasY = e.clientY - rect.top;
  508. const {x: mouseX, y: mouseY} = toImageCoord(canvasX, canvasY);
  509. if (drawing) {
  510. drawing = false;
  511. // 限制endX、endY不出界
  512. endX = Math.max(0, Math.min(mouseX, imgWidth));
  513. endY = Math.max(0, Math.min(mouseY, imgHeight));
  514. const x = Math.max(0, Math.min(startX, endX));
  515. const y = Math.max(0, Math.min(startY, endY));
  516. let w = Math.abs(endX - startX);
  517. let h = Math.abs(endY - startY);
  518. // 再次限制宽高不超界
  519. if (x + w > imgWidth) w = imgWidth - x;
  520. if (y + h > imgHeight) h = imgHeight - y;
  521. if (w > 5 && h > 5) {
  522. boxes.push({ x, y, w, h, classId: 0 });
  523. }
  524. drawAll();
  525. updateBoxesList();
  526. } else if (selectedBoxIndex !== -1 && dragMode) {
  527. dragMode = null;
  528. resizeHandle = null;
  529. }
  530. });
  531. // 删除按钮事件
  532. $('#boxes-list').on('click', '.del-btn', function() {
  533. const idx = $(this).data('idx');
  534. boxes.splice(idx, 1);
  535. if (selectedBoxIndex === idx) selectedBoxIndex = -1;
  536. drawAll();
  537. updateBoxesList();
  538. });
  539. // 导出YOLO格式标注
  540. $('#exportBtn').on('click', function() {
  541. // YOLO格式: class x_center y_center width height (均为归一化)
  542. let lines = boxes.map(box => {
  543. const x_center = (box.x + box.w / 2) / imgWidth;
  544. const y_center = (box.y + box.h / 2) / imgHeight;
  545. const w = box.w / imgWidth;
  546. const h = box.h / imgHeight;
  547. return `${box.classId} ${x_center} ${y_center} ${w} ${h}`;
  548. });
  549. const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
  550. const a = document.createElement('a');
  551. a.href = URL.createObjectURL(blob);
  552. a.download = 'labels.txt';
  553. a.click();
  554. });
  555. // 清空所有标记按钮
  556. $('#clearBoxesBtn').on('click', function() {
  557. boxes = [];
  558. drawAll();
  559. updateBoxesList();
  560. });
  561. // 缩放按钮事件
  562. $('#zoomInBtn').on('click', function() {
  563. scale = Math.min(maxScale, scale * 1.2);
  564. drawAll();
  565. });
  566. $('#zoomOutBtn').on('click', function() {
  567. scale = Math.max(minScale, scale / 1.2);
  568. drawAll();
  569. });
  570. $('#zoomResetBtn').on('click', function() {
  571. scale = 1.0;
  572. offsetX = 0;
  573. offsetY = 0;
  574. drawAll();
  575. });
  576. // 鼠标滚轮缩放
  577. $('#canvas').on('wheel', function(e) {
  578. e.preventDefault();
  579. let mouseX = e.offsetX, mouseY = e.offsetY;
  580. // 以鼠标为中心缩放
  581. let {x: imgX, y: imgY} = toImageCoord(mouseX, mouseY);
  582. let oldScale = scale;
  583. if (e.originalEvent.deltaY < 0) {
  584. scale = Math.min(maxScale, scale * 1.1);
  585. } else {
  586. scale = Math.max(minScale, scale / 1.1);
  587. }
  588. // 修正:缩放后让imgX、imgY依然在(mouseX, mouseY)位置
  589. let domW = $('#canvas').width();
  590. let domH = $('#canvas').height();
  591. let ratioX = imgWidth / domW;
  592. let ratioY = imgHeight / domH;
  593. offsetX = (mouseX * ratioX) / scale - imgX;
  594. offsetY = (mouseY * ratioY) / scale - imgY;
  595. drawAll();
  596. });
  597. // 禁止右键菜单弹出
  598. $('#canvas').on('contextmenu', function(e) { e.preventDefault(); });
  599. });
  600. </script>
  601. </body>
  602. </html>