1
0

foundation.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. import BaseFoundation, { DefaultAdapter } from "../base/foundation";
  2. import { getMiddle, getAspectHW } from "./utils";
  3. export interface CropperAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  4. getContainer: () => HTMLElement;
  5. notifyZoomChange: (zoom: number) => void;
  6. getImg: () => HTMLImageElement
  7. }
  8. interface Point {
  9. x: number;
  10. y: number
  11. }
  12. export interface ImageData {
  13. originalWidth: number;
  14. originalHeight: number;
  15. scale: number
  16. }
  17. export interface ImageDataState {
  18. width: number;
  19. height: number;
  20. centerPoint: Point
  21. }
  22. export interface CropperBox {
  23. width: number;
  24. height: number;
  25. centerPoint: Point
  26. }
  27. export interface ContainerData {
  28. width: number;
  29. height: number
  30. }
  31. export interface CropperBoxBorder {
  32. borderTop: number;
  33. borderLeft: number
  34. }
  35. export default class CropperFoundation <P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<CropperAdapter<P, S>, P, S> {
  36. imgData: ImageData;
  37. containerData: ContainerData;
  38. boxMoveDir: string;
  39. cropperBoxMoveStart: Point;
  40. imgMoveStart: Point;
  41. moveRange: {
  42. xMax: number;
  43. xMin: number;
  44. yMax: number;
  45. yMin: number
  46. };
  47. boxMoveParam: {
  48. paramX: number;
  49. paramY: number
  50. }
  51. cropperBox: CropperBoxBorder;
  52. rangeX: [number, number];
  53. rangeY: [number, number];
  54. initial: boolean;
  55. constructor(adapter: CropperAdapter<P, S>) {
  56. super({ ...adapter });
  57. this.containerData = {} as ContainerData;
  58. this.imgData = {} as ImageData;
  59. this.boxMoveDir = '';
  60. this.boxMoveParam = {
  61. paramX: 0,
  62. paramY: 0,
  63. };
  64. this.rangeX = null;
  65. this.rangeY = null;
  66. this.initial = false;
  67. }
  68. init() {
  69. // 获取容器的宽高
  70. // get cropping Container 's width & height
  71. const container = this._adapter.getContainer();
  72. this.containerData.width = container.clientWidth;
  73. this.containerData.height = container.clientHeight;
  74. this.cropperBoxMoveStart = null;
  75. }
  76. destroy() {
  77. this.unBindMoveEvent();
  78. this.unBindResizeEvent();
  79. }
  80. getImgDataWhenResize = (ratio: number) => {
  81. const { imgData } = this.getStates();
  82. const newImgData = {
  83. width: imgData.width * ratio,
  84. height: imgData.height * ratio,
  85. centerPoint: {
  86. x: imgData.centerPoint.x * ratio,
  87. y: imgData.centerPoint.y * ratio,
  88. }
  89. };
  90. this.imgData.scale *= ratio;
  91. return newImgData;
  92. }
  93. getCropperBoxWhenResize = (ratio: number, newContainerData: ContainerData) => {
  94. const { cropperBox } = this.getStates();
  95. const { aspectRatio } = this.getProps();
  96. const tempCropperBox = {
  97. width: cropperBox.width * ratio,
  98. height: cropperBox.height * ratio,
  99. centerPoint: {
  100. x: cropperBox.centerPoint.x * ratio,
  101. y: cropperBox.centerPoint.y * ratio,
  102. }
  103. };
  104. let xMin = tempCropperBox.centerPoint.x - tempCropperBox.width / 2;
  105. let xMax = tempCropperBox.centerPoint.x + tempCropperBox.width / 2;
  106. let yMin = tempCropperBox.centerPoint.y - tempCropperBox.height / 2;
  107. let yMax = tempCropperBox.centerPoint.y + tempCropperBox.height / 2;
  108. if (aspectRatio) {
  109. if (xMax > newContainerData.width) {
  110. xMax = newContainerData.width;
  111. xMin = tempCropperBox.width > newContainerData.width ?
  112. 0 : newContainerData.width - tempCropperBox.width;
  113. tempCropperBox.width = xMax - xMin;
  114. tempCropperBox.height = tempCropperBox.width / aspectRatio;
  115. yMax = yMin + tempCropperBox.height;
  116. }
  117. if (yMax > newContainerData.height) {
  118. yMax = newContainerData.height;
  119. yMin = tempCropperBox.height > newContainerData.height ?
  120. 0 : newContainerData.height - tempCropperBox.height;
  121. tempCropperBox.height = yMax - yMin;
  122. tempCropperBox.width = tempCropperBox.height * aspectRatio;
  123. xMax = xMin + tempCropperBox.width;
  124. }
  125. } else {
  126. if (xMax > newContainerData.width) {
  127. xMax = newContainerData.width;
  128. xMin = tempCropperBox.width > newContainerData.width ?
  129. 0 : newContainerData.width - tempCropperBox.width;
  130. }
  131. if (yMax > newContainerData.height) {
  132. yMax = newContainerData.height;
  133. yMin = tempCropperBox.height > newContainerData.height ?
  134. 0 : newContainerData.height - tempCropperBox.height;
  135. }
  136. }
  137. return {
  138. width: xMax - xMin,
  139. height: yMax - yMin,
  140. centerPoint: {
  141. x: (xMax + xMin) / 2,
  142. y: (yMax + yMin) / 2,
  143. }
  144. };
  145. }
  146. handleResize = () => {
  147. const { loaded } = this.getStates();
  148. if (!this.initial) {
  149. this.initial = true;
  150. return;
  151. }
  152. if (!loaded) {
  153. return;
  154. }
  155. const container = this._adapter.getContainer();
  156. const newContainerData = {
  157. width: container.clientWidth,
  158. height: container.clientHeight,
  159. };
  160. const ratio = newContainerData.width / this.containerData.width;
  161. const newImgData = this.getImgDataWhenResize(ratio);
  162. const newCropperBox = this.getCropperBoxWhenResize(ratio, newContainerData);
  163. this.containerData = newContainerData;
  164. this.setState({
  165. imgData: newImgData,
  166. cropperBox: newCropperBox,
  167. } as any);
  168. }
  169. handleImageLoad = (e: any) => {
  170. /**
  171. * 1. 图片加载完成后,获得图片的原始大小
  172. * 2. 计算图片的缩放比例,中心点位置
  173. */
  174. const { naturalWidth, naturalHeight } = e.target;
  175. const { width: containerWidth, height: containerHeight } = this.containerData;
  176. this.imgData.originalWidth = naturalWidth;
  177. this.imgData.originalHeight = naturalHeight;
  178. let scale = 1;
  179. const newImgDataState = {} as ImageDataState;
  180. /* 计算图片加载后的初始显示尺寸 */
  181. if (naturalWidth / containerWidth > naturalHeight / containerHeight) {
  182. scale = containerWidth / naturalWidth;
  183. newImgDataState.width = containerWidth;
  184. newImgDataState.height = naturalHeight * scale;
  185. } else {
  186. scale = containerHeight / naturalHeight;
  187. newImgDataState.width = naturalWidth * scale;
  188. newImgDataState.height = containerHeight;
  189. }
  190. this.imgData.scale = scale;
  191. newImgDataState.centerPoint = {} as Point;
  192. newImgDataState.centerPoint.x = containerWidth / 2;
  193. newImgDataState.centerPoint.y = containerHeight / 2;
  194. /* 计算裁切框大小 */
  195. const newCropperBoxState = {} as CropperBox;
  196. const { defaultAspectRatio, aspectRatio } = this.getProps();
  197. const calcAspect = aspectRatio || defaultAspectRatio;
  198. if (containerWidth / containerHeight > calcAspect) {
  199. newCropperBoxState.width = containerHeight * calcAspect;
  200. newCropperBoxState.height = containerHeight;
  201. } else {
  202. newCropperBoxState.width = containerWidth;
  203. newCropperBoxState.height = containerWidth / calcAspect;
  204. }
  205. newCropperBoxState.centerPoint = {} as Point;
  206. newCropperBoxState.centerPoint.x = containerWidth / 2;
  207. newCropperBoxState.centerPoint.y = containerHeight / 2;
  208. this.setState({
  209. imgData: newImgDataState,
  210. cropperBox: newCropperBoxState,
  211. loaded: true,
  212. } as any);
  213. }
  214. handleWheel = (e: any) => {
  215. // 防止双手缩放导致页面被放大
  216. e.preventDefault();
  217. const { imgData, zoom: currZoom } = this.getStates();
  218. const { maxZoom, minZoom, zoomStep } = this.getProps();
  219. let _zoom: number;
  220. if (e.deltaY < 0) {
  221. /* zoom in */
  222. if (currZoom + zoomStep <= maxZoom) {
  223. _zoom = Number((currZoom + zoomStep).toFixed(2));
  224. }
  225. } else if (e.deltaY > 0) {
  226. /* zoom out */
  227. if (currZoom - zoomStep >= minZoom) {
  228. _zoom = Number((currZoom - zoomStep).toFixed(2));
  229. }
  230. }
  231. if (_zoom === undefined) {
  232. return;
  233. }
  234. const boundingRect = e.currentTarget.getBoundingClientRect();
  235. const offsetX = e.clientX - boundingRect.left;
  236. const offsetY = e.clientY - boundingRect.top;
  237. const scaleCenter = {
  238. x: offsetX,
  239. y: - offsetY,
  240. };
  241. // 计算新的中心点位置
  242. const currentPoint = { ...imgData.centerPoint } as Point;
  243. currentPoint.y = - currentPoint.y;
  244. const newCenterPoint = {
  245. x: (currentPoint.x - scaleCenter.x) / currZoom * _zoom + scaleCenter.x,
  246. y: - [(currentPoint.y - scaleCenter.y) / currZoom * _zoom + scaleCenter.y],
  247. };
  248. const newWidth = imgData.width / currZoom * _zoom;
  249. const newHeight = imgData.height / currZoom * _zoom;
  250. const newImgDataState = {
  251. width: newWidth,
  252. height: newHeight,
  253. centerPoint: newCenterPoint
  254. };
  255. this.setState({
  256. imgData: newImgDataState,
  257. zoom: _zoom
  258. } as any);
  259. this._adapter.notifyZoomChange(_zoom);
  260. }
  261. getMoveParamByDir(dir: string) {
  262. let paramX = 0, paramY = 0;
  263. switch (dir) {
  264. case 'tl':
  265. paramX = -1; paramY = -1; break;
  266. case 'tm':
  267. paramY = -1; break;
  268. case 'tr':
  269. paramX = 1; paramY = -1; break;
  270. case 'ml':
  271. paramX = -1; break;
  272. case 'mr':
  273. paramX = 1; break;
  274. case 'bl':
  275. paramX = -1; paramY = 1; break;
  276. case 'bm':
  277. paramY = 1; break;
  278. case 'br':
  279. paramX = 1; paramY = 1; break;
  280. default:
  281. break;
  282. }
  283. return {
  284. paramX,
  285. paramY
  286. };
  287. }
  288. getRangeForAspectChange = () => {
  289. const { cropperBox } = this.getStates();
  290. const { aspectRatio } = this.getProps();
  291. const { width: containerWidth, height: containerHeight } = this.containerData;
  292. // 可能的最大宽高
  293. let height: number, width: number;
  294. // 裁剪框当前的位置
  295. const xMin = cropperBox.centerPoint.x - cropperBox.width / 2;
  296. const xMax = cropperBox.centerPoint.x + cropperBox.width / 2;
  297. const yMin = cropperBox.centerPoint.y - cropperBox.height / 2;
  298. const yMax = cropperBox.centerPoint.y + cropperBox.height / 2;
  299. switch (this.boxMoveDir) {
  300. case 'tl':
  301. height = yMax;
  302. width = xMax;
  303. [width, height] = getAspectHW(width, height, aspectRatio);
  304. this.rangeX = [xMax - width, xMax];
  305. this.rangeY = [yMax - height, yMax];
  306. break;
  307. case 'tm':
  308. height = yMax;
  309. const leftHalfWidth = cropperBox.centerPoint.x;
  310. const rightHalfWidth = containerWidth - cropperBox.centerPoint.x;
  311. width = 2 * (leftHalfWidth < rightHalfWidth ? leftHalfWidth : rightHalfWidth);
  312. [width, height] = getAspectHW(width, height, aspectRatio);
  313. this.rangeX = [
  314. cropperBox.centerPoint.x - width / 2,
  315. cropperBox.centerPoint.x + width / 2
  316. ];
  317. this.rangeY = [yMax - height, yMax];
  318. break;
  319. case 'tr':
  320. height = yMax;
  321. width = containerWidth - xMin;
  322. [width, height] = getAspectHW(width, height, aspectRatio);
  323. this.rangeX = [xMin, xMin + width];
  324. this.rangeY = [yMax - height, yMax];
  325. break;
  326. case 'ml':
  327. width = xMax;
  328. const topHalfHeight = cropperBox.centerPoint.y;
  329. const bottomHalfHeight = containerHeight - cropperBox.centerPoint.y;
  330. height = 2 * (topHalfHeight < bottomHalfHeight ? topHalfHeight : bottomHalfHeight);
  331. [width, height] = getAspectHW(width, height, aspectRatio);
  332. this.rangeX = [xMax - width, xMax];
  333. this.rangeY = [
  334. cropperBox.centerPoint.y - height / 2,
  335. cropperBox.centerPoint.y + height / 2
  336. ];
  337. break;
  338. case 'mr':
  339. width = containerWidth - xMin;
  340. const topHalfHeight2 = cropperBox.centerPoint.y;
  341. const bottomHalfHeight2 = containerHeight - cropperBox.centerPoint.y;
  342. height = 2 * (topHalfHeight2 < bottomHalfHeight2 ? topHalfHeight2 : bottomHalfHeight2);
  343. [width, height] = getAspectHW(width, height, aspectRatio);
  344. this.rangeX = [xMin, xMin + width];
  345. this.rangeY = [
  346. cropperBox.centerPoint.y - height / 2,
  347. cropperBox.centerPoint.y + height / 2
  348. ];
  349. break;
  350. case 'bl':
  351. height = containerHeight - yMin;
  352. width = xMax;
  353. [width, height] = getAspectHW(width, height, aspectRatio);
  354. this.rangeX = [xMax - width, xMax];
  355. this.rangeY = [yMin, yMin + height];
  356. break;
  357. case 'bm':
  358. height = containerHeight - yMin;
  359. const leftHalfWidth2 = cropperBox.centerPoint.x;
  360. const rightHalfWidth2 = containerWidth - cropperBox.centerPoint.x;
  361. width = 2 * (leftHalfWidth2 < rightHalfWidth2 ? leftHalfWidth2 : rightHalfWidth2);
  362. [width, height] = getAspectHW(width, height, aspectRatio);
  363. this.rangeX = [
  364. cropperBox.centerPoint.x - width / 2,
  365. cropperBox.centerPoint.x + width / 2,
  366. ];
  367. this.rangeY = [yMin, yMin + height];
  368. break;
  369. case 'br':
  370. height = containerHeight - yMin;
  371. width = containerWidth - xMin;
  372. [width, height] = getAspectHW(width, height, aspectRatio);
  373. this.rangeX = [xMin, xMin + width];
  374. this.rangeY = [yMin, yMin + height];
  375. break;
  376. default:
  377. break;
  378. }
  379. }
  380. handleCornerMouseDown = (e: any) => {
  381. const currentTarget = e.currentTarget;
  382. if (!currentTarget) {
  383. return;
  384. }
  385. e.preventDefault();
  386. const dir = currentTarget.dataset.dir;
  387. this.boxMoveDir = dir;
  388. this.boxMoveParam = this.getMoveParamByDir(dir);
  389. this.bindResizeEvent();
  390. const { aspectRatio } = this.getProps();
  391. if (aspectRatio) {
  392. this.getRangeForAspectChange();
  393. } else {
  394. this.rangeX = [0, this.containerData.width];
  395. this.rangeY = [0, this.containerData.height];
  396. }
  397. }
  398. bindResizeEvent = () => {
  399. const { aspectRatio } = this.getProps();
  400. document.addEventListener('mousemove', aspectRatio ? this.handleCornerAspectMouseMove : this.handleCornerMouseMove);
  401. document.addEventListener('mouseup', this.handleCornerMouseUp);
  402. }
  403. unBindResizeEvent = () => {
  404. const { aspectRatio } = this.getProps();
  405. document.removeEventListener('mousemove', aspectRatio ? this.handleCornerAspectMouseMove : this.handleCornerMouseMove);
  406. document.removeEventListener('mouseup', this.handleCornerMouseUp);
  407. }
  408. viewIMGDragStart = (e: any) => {
  409. e.preventDefault();
  410. }
  411. handleCornerAspectMouseMove = (e: any) => {
  412. e.preventDefault();
  413. const { clientX, clientY } = e;
  414. const { cropperBox } = this.getStates();
  415. const { aspectRatio } = this.getProps();
  416. const boundingRect = this._adapter.getContainer().getBoundingClientRect();
  417. const newCropperBoxPos = {
  418. width: cropperBox.width,
  419. height: cropperBox.height,
  420. centerPoint: { ...cropperBox.centerPoint }
  421. };
  422. let offsetX: number, offsetY: number;
  423. if (['ml', 'mr'].includes(this.boxMoveDir)) {
  424. offsetX = getMiddle(clientX - boundingRect.left, this.rangeX);
  425. } else {
  426. offsetY = getMiddle(clientY - boundingRect.top, this.rangeY);
  427. }
  428. switch (this.boxMoveDir) {
  429. case 'tl':
  430. newCropperBoxPos.height = this.rangeY[1] - offsetY;
  431. newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
  432. newCropperBoxPos.centerPoint = {
  433. x: this.rangeX[1] - newCropperBoxPos.width / 2,
  434. y: this.rangeY[1] - newCropperBoxPos.height / 2,
  435. };
  436. break;
  437. case 'tm':
  438. newCropperBoxPos.height = this.rangeY[1] - offsetY;
  439. newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
  440. newCropperBoxPos.centerPoint = {
  441. x: cropperBox.centerPoint.x,
  442. y: this.rangeY[1] - newCropperBoxPos.height / 2,
  443. };
  444. break;
  445. case 'tr':
  446. newCropperBoxPos.height = this.rangeY[1] - offsetY;
  447. newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
  448. newCropperBoxPos.centerPoint = {
  449. x: this.rangeX[0] + newCropperBoxPos.width / 2,
  450. y: this.rangeY[1] - newCropperBoxPos.height / 2,
  451. };
  452. break;
  453. case 'ml':
  454. newCropperBoxPos.width = this.rangeX[1] - offsetX;
  455. newCropperBoxPos.height = newCropperBoxPos.width / aspectRatio;
  456. newCropperBoxPos.centerPoint = {
  457. x: this.rangeX[1] - newCropperBoxPos.width / 2,
  458. y: cropperBox.centerPoint.y,
  459. };
  460. break;
  461. case 'mr':
  462. newCropperBoxPos.width = offsetX - this.rangeX[0];
  463. newCropperBoxPos.height = newCropperBoxPos.width / aspectRatio;
  464. newCropperBoxPos.centerPoint = {
  465. x: this.rangeX[0] + newCropperBoxPos.width / 2,
  466. y: cropperBox.centerPoint.y,
  467. };
  468. break;
  469. case 'bl':
  470. newCropperBoxPos.height = offsetY - this.rangeY[0];
  471. newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
  472. newCropperBoxPos.centerPoint = {
  473. x: this.rangeX[1] - newCropperBoxPos.width / 2,
  474. y: this.rangeY[0] + newCropperBoxPos.height / 2,
  475. };
  476. break;
  477. case 'bm':
  478. newCropperBoxPos.height = offsetY - this.rangeY[0];
  479. newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
  480. newCropperBoxPos.centerPoint = {
  481. x: cropperBox.centerPoint.x,
  482. y: this.rangeY[0] + newCropperBoxPos.height / 2,
  483. };
  484. break;
  485. case 'br':
  486. newCropperBoxPos.height = offsetY - this.rangeY[0];
  487. newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
  488. newCropperBoxPos.centerPoint = {
  489. x: this.rangeX[0] + newCropperBoxPos.width / 2,
  490. y: this.rangeY[0] + newCropperBoxPos.height / 2,
  491. };
  492. break;
  493. default:
  494. break;
  495. }
  496. if (newCropperBoxPos.height === 0 && newCropperBoxPos.width === 0) {
  497. this.changeDir();
  498. this.getRangeForAspectChange();
  499. }
  500. this.setState({
  501. cropperBox: newCropperBoxPos
  502. } as any);
  503. }
  504. changeDir = () => {
  505. if (this.boxMoveDir.includes('t')) {
  506. this.boxMoveDir = this.boxMoveDir.replace('t', 'b');
  507. } else if (this.boxMoveDir.includes('b')) {
  508. this.boxMoveDir = this.boxMoveDir.replace('b', 't');
  509. }
  510. if (this.boxMoveDir.includes('l')) {
  511. this.boxMoveDir = this.boxMoveDir.replace('l', 'r');
  512. } else if (this.boxMoveDir.includes('r')) {
  513. this.boxMoveDir = this.boxMoveDir.replace('r', 'l');
  514. }
  515. }
  516. handleCornerMouseMove = (e: any) => {
  517. e.preventDefault();
  518. const { clientX, clientY } = e;
  519. const { cropperBox } = this.getStates();
  520. const boundingRect = this._adapter.getContainer().getBoundingClientRect();
  521. let offsetX = getMiddle(clientX - boundingRect.left, this.rangeX);
  522. let offsetY = getMiddle(clientY - boundingRect.top, this.rangeY);
  523. const newCropperBoxPos = {
  524. width: cropperBox.width,
  525. height: cropperBox.height,
  526. centerPoint: {
  527. x: cropperBox.centerPoint.x,
  528. y: cropperBox.centerPoint.y
  529. }
  530. };
  531. const { paramX, paramY } = this.boxMoveParam;
  532. let x: number, y: number;
  533. if (paramX) {
  534. x = cropperBox.centerPoint.x + paramX * cropperBox.width / 2;
  535. newCropperBoxPos.width = cropperBox.width + paramX * (offsetX - x);
  536. if (newCropperBoxPos.width < 0) {
  537. newCropperBoxPos.width = - newCropperBoxPos.width;
  538. this.boxMoveParam.paramX = -paramX;
  539. }
  540. newCropperBoxPos.centerPoint.x = offsetX - paramX * newCropperBoxPos.width / 2;
  541. }
  542. if (paramY) {
  543. y = cropperBox.centerPoint.y + paramY * cropperBox.height / 2;
  544. newCropperBoxPos.height = cropperBox.height + paramY * (offsetY - y);
  545. if (newCropperBoxPos.height < 0) {
  546. newCropperBoxPos.height = -newCropperBoxPos.height;
  547. this.boxMoveParam.paramY = -paramY;
  548. }
  549. newCropperBoxPos.centerPoint.y = offsetY - paramY * newCropperBoxPos.height / 2;
  550. }
  551. this.setState({
  552. cropperBox: newCropperBoxPos
  553. } as any);
  554. }
  555. handleCornerMouseUp = (e: any) => {
  556. this.boxMoveParam = { paramX: 0, paramY: 0 };
  557. this.unBindResizeEvent();
  558. }
  559. handleCropperBoxMouseDown = (e: any) => {
  560. const target = e.target;
  561. const { cropperBox } = this.getStates();
  562. const container = this._adapter.getContainer();
  563. const boundingRect = container.getBoundingClientRect();
  564. if (target.dataset.dir) {
  565. // 如果鼠标是落在了corner上,那么不做任何操作
  566. return;
  567. }
  568. // 移动裁切框
  569. this.cropperBoxMoveStart = {
  570. x: e.clientX,
  571. y: e.clientY
  572. };
  573. this.bindMoveEvent();
  574. // 计算 cropperBox 中心点移动范围
  575. this.moveRange = {
  576. xMin: cropperBox.width / 2,
  577. xMax: boundingRect.width - cropperBox.width / 2,
  578. yMin: cropperBox.height / 2,
  579. yMax: boundingRect.height - cropperBox.height / 2,
  580. };
  581. }
  582. bindMoveEvent = () => {
  583. document.addEventListener('mousemove', this.handleCropperBoxMouseMove);
  584. document.addEventListener('mouseup', this.handleCropperBoxMouseUp);
  585. }
  586. unBindMoveEvent = () => {
  587. document.removeEventListener('mousemove', this.handleCropperBoxMouseMove);
  588. document.removeEventListener('mouseup', this.handleCropperBoxMouseUp);
  589. }
  590. handleCropperBoxMouseMove = (e: any) => {
  591. if (!this.cropperBoxMoveStart) {
  592. return;
  593. }
  594. const { clientX, clientY } = e;
  595. const { cropperBox } = this.getStates();
  596. const offsetX = clientX - this.cropperBoxMoveStart.x;
  597. const offsetY = clientY - this.cropperBoxMoveStart.y;
  598. const newCenterPointX = getMiddle(cropperBox.centerPoint.x + offsetX, [this.moveRange.xMin, this.moveRange.xMax]);
  599. const newCenterPointY = getMiddle(cropperBox.centerPoint.y + offsetY, [this.moveRange.yMin, this.moveRange.yMax]);
  600. const newCropperBoxPos = {
  601. width: cropperBox.width,
  602. height: cropperBox.height,
  603. centerPoint: {
  604. x: newCenterPointX,
  605. y: newCenterPointY
  606. }
  607. };
  608. this.cropperBoxMoveStart = {
  609. x: clientX,
  610. y: clientY
  611. };
  612. this.setState({
  613. cropperBox: newCropperBoxPos
  614. } as any);
  615. }
  616. handleCropperBoxMouseUp = (e: any) => {
  617. if (!this.cropperBoxMoveStart) {
  618. return;
  619. }
  620. this.cropperBoxMoveStart = null;
  621. this.unBindMoveEvent();
  622. }
  623. handleMaskMouseDown = (e: any) => {
  624. if (e.currentTarget !== e.target) {
  625. return;
  626. }
  627. this.bindImgMoveEvent();
  628. // 记录开始移动的位置
  629. this.imgMoveStart = {
  630. x: e.clientX,
  631. y: e.clientY
  632. };
  633. }
  634. bindImgMoveEvent = () => {
  635. document.addEventListener('mousemove', this.handleImgMove);
  636. document.addEventListener('mouseup', this.handleImgMoveUp);
  637. }
  638. unBindImgMoveEvent = () => {
  639. document.removeEventListener('mousemove', this.handleImgMove);
  640. document.removeEventListener('mouseup', this.handleImgMoveUp);
  641. }
  642. handleImgMove = (e: any) => {
  643. if (!this.imgMoveStart) {
  644. return;
  645. }
  646. const { clientX, clientY } = e;
  647. const { imgData } = this.getStates();
  648. const offsetX = clientX - this.imgMoveStart.x;
  649. const offsetY = clientY - this.imgMoveStart.y;
  650. const newCenterPointX = imgData.centerPoint.x + offsetX;
  651. const newCenterPointY = imgData.centerPoint.y + offsetY;
  652. const newImgData = {
  653. width: imgData.width,
  654. height: imgData.height,
  655. centerPoint: {
  656. x: newCenterPointX,
  657. y: newCenterPointY
  658. }
  659. };
  660. this.imgMoveStart = {
  661. x: clientX,
  662. y: clientY
  663. };
  664. this.setState({
  665. imgData: newImgData
  666. } as any);
  667. }
  668. handleImgMoveUp = (e: any) => {
  669. if (!this.imgMoveStart) {
  670. return;
  671. }
  672. this.imgMoveStart = null;
  673. this.unBindImgMoveEvent();
  674. }
  675. getCropperCanvas = () => {
  676. const { cropperBox, imgData, rotate, zoom } = this.getStates();
  677. const { fill } = this.getProps();
  678. const canvas = document.createElement('canvas');
  679. const ctx = canvas.getContext('2d');
  680. const img = this._adapter.getImg();
  681. // 计算包含旋转后的图片的矩形容器的宽高
  682. const angle = rotate * Math.PI / 180;
  683. const sine = Math.abs(Math.sin(angle));
  684. const cosine = Math.abs(Math.cos(angle));
  685. const imgWidth = this.imgData.originalWidth;
  686. const imgHeight = this.imgData.originalHeight;
  687. const containerWidth = imgWidth * cosine + imgHeight * sine;
  688. const containerHeight = imgHeight * cosine + imgWidth * sine;
  689. // 判断裁切区域和外接矩形是否存在交集,如果不存在,则直接返回空白图片
  690. // 计算需要裁剪的区域实际大小和位置
  691. const cropperContainerWidth = containerWidth * zoom * this.imgData.scale;
  692. const cropperContainerHeight = containerHeight * zoom * this.imgData.scale;
  693. const cropperContainerTop = imgData.centerPoint.y - cropperContainerHeight / 2;
  694. const cropperContainerLeft = imgData.centerPoint.x - cropperContainerWidth / 2;
  695. const cropperBoxLeft = cropperBox.centerPoint.x - cropperBox.width / 2;
  696. const cropperBoxTop = cropperBox.centerPoint.y - cropperBox.height / 2;
  697. const realZoom = zoom * this.imgData.scale;
  698. const relativeCropLeft = (cropperBoxLeft - cropperContainerLeft) / realZoom;
  699. const relativeCropTop = (cropperBoxTop - cropperContainerTop) / realZoom;
  700. const relativeWidth = cropperBox.width / realZoom;
  701. const relativeHeight = cropperBox.height / realZoom;
  702. const relativeCropRight = relativeCropLeft + relativeWidth;
  703. const relativeCropBottom = relativeCropTop + relativeHeight;
  704. if (relativeCropRight < 0 || relativeCropBottom < 0 || relativeCropLeft > containerWidth || relativeCropTop > containerHeight) {
  705. // 没有交集,直接返回空白图片
  706. const emptyCanvas = document.createElement('canvas');
  707. const ctx = emptyCanvas.getContext('2d');
  708. emptyCanvas.width = relativeWidth;
  709. emptyCanvas.height = relativeHeight;
  710. ctx.fillStyle = fill;
  711. ctx.fillRect(0, 0, relativeWidth, relativeHeight);
  712. return emptyCanvas;
  713. }
  714. canvas.width = containerWidth;
  715. canvas.height = containerHeight;
  716. ctx.fillStyle = fill;
  717. ctx.fillRect(0, 0, containerWidth, containerHeight);
  718. const halfWidth = containerWidth / 2;
  719. const halfHeight = containerHeight / 2;
  720. ctx.translate(halfWidth, halfHeight);
  721. ctx.rotate(rotate * Math.PI / 180);
  722. ctx.translate(-halfWidth, -halfHeight);
  723. const imgX = (containerWidth - imgWidth) / 2;
  724. const imgY = (containerHeight - imgHeight) / 2;
  725. ctx.drawImage(img, 0, 0, imgWidth, imgHeight, imgX, imgY, imgWidth, imgHeight);
  726. const canvas2 = document.createElement('canvas');
  727. const ctx2 = canvas2.getContext('2d');
  728. // 为了避免裁剪时候,超出被裁切的画布的部分颜色不正常,需要将裁切区域限制在画布范围内。
  729. // 相对位置会在后续进行修正
  730. let realLeft = relativeCropLeft;
  731. let realTop = relativeCropTop;
  732. let realWidth = relativeWidth;
  733. let realHeight = relativeHeight;
  734. if (relativeCropLeft < 0) {
  735. realLeft = 0;
  736. }
  737. if (relativeCropTop < 0) {
  738. realTop = 0;
  739. }
  740. if (relativeCropRight > containerWidth) {
  741. realWidth = containerWidth - realLeft;
  742. } else if (relativeCropLeft < 0) {
  743. realWidth = relativeCropRight;
  744. }
  745. if (relativeCropBottom > containerHeight) {
  746. realHeight = containerHeight - realTop;
  747. } else if (relativeCropTop < 0) {
  748. realHeight = relativeCropBottom;
  749. }
  750. const imgDataResult = ctx.getImageData(realLeft, realTop, realWidth, realHeight);
  751. canvas2.width = relativeWidth;
  752. canvas2.height = relativeHeight;
  753. ctx2.fillStyle = fill;
  754. ctx2.fillRect(0, 0, relativeWidth, relativeHeight);
  755. ctx2.putImageData(
  756. imgDataResult,
  757. relativeCropLeft < 0 ? - relativeCropLeft : 0,
  758. relativeCropTop < 0 ? - relativeCropTop : 0,
  759. );
  760. return canvas2;
  761. }
  762. }