qf-image-cropper.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. <template>
  2. <view class="image-cropper" :style="{ zIndex }" @wheel="cropper.mousewheel">
  3. <canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
  4. width: `${canvansWidth}px`,
  5. height: `${canvansHeight}px`
  6. }"></canvas>
  7. <canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
  8. width: `${canvansWidth}px`,
  9. height: `${canvansHeight}px`
  10. }"></canvas>
  11. <view id="pic-preview" class="pic-preview" :change:init="cropper.initObserver" :init="initData" @touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
  12. <image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
  13. <view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
  14. <view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
  15. <view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
  16. <view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
  17. </view>
  18. <block v-if="showGrid">
  19. <view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
  20. </block>
  21. <block v-if="showAngle">
  22. <view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
  23. <view :style="[{
  24. width: `${angleSize}px`,
  25. height: `${angleSize}px`
  26. }]"></view>
  27. </view>
  28. </block>
  29. </view>
  30. <slot />
  31. <view class="fixed-bottom safe-area-inset-bottom" :style="{ zIndex: initData.area.zIndex + 99 }">
  32. <view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar">
  33. <view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view>
  34. <view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view>
  35. </view>
  36. <view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
  37. <block v-else-if="!!imgSrc">
  38. <view class="rechoose" @click="chooseImage">重选</view>
  39. <button class="button" size="mini" @click="cropClick">确定</button>
  40. </block>
  41. <view v-else class="choose-btn" @click="chooseImage">选择图片</view>
  42. </view>
  43. </view>
  44. </template>
  45. <!-- #ifdef APP-VUE -->
  46. <script module="cropper" lang="renderjs">
  47. import cropper from './qf-image-cropper.render.js';
  48. // vue3 app renderjs中条件编译无效
  49. cropper.setPlatform('APP');
  50. export default {
  51. mixins: [ cropper ]
  52. }
  53. </script>
  54. <!-- #endif -->
  55. <!-- #ifdef H5 -->
  56. <script module="cropper" lang="renderjs">
  57. import cropper from './qf-image-cropper.render.js';
  58. export default {
  59. mixins: [ cropper ]
  60. }
  61. </script>
  62. <!-- #endif -->
  63. <!-- #ifdef MP-WEIXIN || MP-QQ -->
  64. <script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
  65. <!-- #endif -->
  66. <script>
  67. /** 裁剪区域最大宽高所占屏幕宽度百分比 */
  68. const AREA_SIZE = 75;
  69. /** 图片默认宽高 */
  70. const IMG_SIZE = 300;
  71. export default {
  72. name:"qf-image-cropper",
  73. // #ifdef MP-WEIXIN
  74. options: {
  75. // 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响
  76. styleIsolation: "isolated"
  77. },
  78. // #endif
  79. props: {
  80. /** 图片资源地址 */
  81. src: {
  82. type: String,
  83. default: ''
  84. },
  85. /** 裁剪宽度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
  86. width: {
  87. type: Number,
  88. default: IMG_SIZE
  89. },
  90. /** 裁剪高度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
  91. height: {
  92. type: Number,
  93. default: IMG_SIZE
  94. },
  95. /** 是否绘制裁剪区域边框 */
  96. showBorder: {
  97. type: Boolean,
  98. default: true
  99. },
  100. /** 是否绘制裁剪区域网格参考线 */
  101. showGrid: {
  102. type: Boolean,
  103. default: true
  104. },
  105. /** 是否展示四个支持伸缩的角 */
  106. showAngle: {
  107. type: Boolean,
  108. default: true
  109. },
  110. /** 裁剪区域最小缩放倍数 */
  111. areaScale: {
  112. type: Number,
  113. default: 0.3
  114. },
  115. /** 图片最小缩放倍数 */
  116. minScale: {
  117. type: Number,
  118. default: 1
  119. },
  120. /** 图片最大缩放倍数 */
  121. maxScale: {
  122. type: Number,
  123. default: 5
  124. },
  125. /** 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 */
  126. checkRange: {
  127. type: Boolean,
  128. default: true
  129. },
  130. /** 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块 */
  131. backgroundColor: {
  132. type: String
  133. },
  134. /** 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 */
  135. bounce: {
  136. type: Boolean,
  137. default: true
  138. },
  139. /** 是否支持翻转 */
  140. rotatable: {
  141. type: Boolean,
  142. default: true
  143. },
  144. /** 是否支持逆向翻转 */
  145. reverseRotatable: {
  146. type: Boolean,
  147. default: false
  148. },
  149. /** 是否支持从本地选择素材 */
  150. choosable: {
  151. type: Boolean,
  152. default: true
  153. },
  154. /** 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 */
  155. gpu: {
  156. type: Boolean,
  157. default: false
  158. },
  159. /** 四个角尺寸,单位px */
  160. angleSize: {
  161. type: Number,
  162. default: 20
  163. },
  164. /** 四个角边框宽度,单位px */
  165. angleBorderWidth: {
  166. type: Number,
  167. default: 2
  168. },
  169. zIndex: {
  170. type: [Number, String]
  171. },
  172. /** 裁剪图片圆角半径,单位px */
  173. radius: {
  174. type: Number,
  175. default: 0
  176. },
  177. /** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
  178. fileType: {
  179. type: String,
  180. default: 'png'
  181. },
  182. /**
  183. * 图片从绘制到生成所需时间,单位ms
  184. * 微信小程序平台使用 `Canvas 2D` 绘制时有效
  185. * 如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间
  186. */
  187. delay: {
  188. type: Number,
  189. default: 1000
  190. },
  191. // #ifdef H5
  192. /**
  193. * 页面是否是原生标题栏
  194. * H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。
  195. * 注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的
  196. */
  197. navigation: {
  198. type: Boolean,
  199. default: true
  200. }
  201. // #endif
  202. },
  203. emits: ["crop"],
  204. data() {
  205. return {
  206. // 用不同 id 使 v-for key 不重复
  207. maskList: [
  208. { id: 'crop-mask-block-1' },
  209. { id: 'crop-mask-block-2' },
  210. { id: 'crop-mask-block-3' },
  211. { id: 'crop-mask-block-4' },
  212. ],
  213. gridList: [
  214. { id: 'crop-grid-1' },
  215. { id: 'crop-grid-2' },
  216. { id: 'crop-grid-3' },
  217. { id: 'crop-grid-4' },
  218. ],
  219. angleList: [
  220. { id: 'crop-angle-1' },
  221. { id: 'crop-angle-2' },
  222. { id: 'crop-angle-3' },
  223. { id: 'crop-angle-4' },
  224. ],
  225. /** 本地缓存的图片路径 */
  226. imgSrc: '',
  227. /** 图片的裁剪宽度 */
  228. imgWidth: IMG_SIZE,
  229. /** 图片的裁剪高度 */
  230. imgHeight: IMG_SIZE,
  231. /** 裁剪区域最大宽度所占屏幕宽度百分比 */
  232. widthPercent: AREA_SIZE,
  233. /** 裁剪区域最大高度所占屏幕宽度百分比 */
  234. heightPercent: AREA_SIZE,
  235. /** 裁剪区域布局信息 */
  236. area: {},
  237. /** 未被缩放过的图片宽 */
  238. oldWidth: 0,
  239. /** 未被缩放过的图片高 */
  240. oldHeight: 0,
  241. /** 系统信息 */
  242. sys: uni.getSystemInfoSync(),
  243. scaleWidth: 0,
  244. scaleHeight: 0,
  245. rotate: 0,
  246. offsetX: 0,
  247. offsetY: 0,
  248. use2d: false,
  249. canvansWidth: 0,
  250. canvansHeight: 0,
  251. // imageStyles: {},
  252. // maskStylesList: [{}, {}, {}, {}],
  253. // borderStyles: {},
  254. // gridStylesList: [{}, {}, {}, {}],
  255. // angleStylesList: [{}, {}, {}, {}],
  256. // circleBoxStyles: {},
  257. // circleStyles: {},
  258. }
  259. },
  260. computed: {
  261. initData() {
  262. // console.log('initData')
  263. return {
  264. timestamp: new Date().getTime(),
  265. area: {
  266. ...this.area,
  267. bounce: this.bounce,
  268. showBorder: this.showBorder,
  269. showGrid: this.showGrid,
  270. showAngle: this.showAngle,
  271. angleSize: this.angleSize,
  272. angleBorderWidth: this.angleBorderWidth,
  273. minScale: this.areaScale,
  274. widthPercent: this.widthPercent,
  275. heightPercent: this.heightPercent,
  276. radius: this.radius,
  277. checkRange: this.checkRange,
  278. zIndex: +this.zIndex || 0,
  279. },
  280. sys: this.sys,
  281. img: {
  282. minScale: this.minScale,
  283. maxScale: this.maxScale,
  284. src: this.imgSrc,
  285. width: this.oldWidth,
  286. height: this.oldHeight,
  287. oldWidth: this.oldWidth,
  288. oldHeight: this.oldHeight,
  289. gpu: this.gpu,
  290. }
  291. }
  292. },
  293. imgProps() {
  294. return {
  295. width: this.width,
  296. height: this.height,
  297. src: this.src,
  298. }
  299. }
  300. },
  301. watch: {
  302. imgProps: {
  303. handler(val, oldVal) {
  304. // 自定义裁剪尺,示例如下:
  305. this.imgWidth = Number(val.width) || IMG_SIZE;
  306. this.imgHeight = Number(val.height) || IMG_SIZE;
  307. let use2d = true;
  308. // #ifndef MP-WEIXIN
  309. use2d = false;
  310. // #endif
  311. // if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
  312. // use2d = false;
  313. // }
  314. let canvansWidth = this.imgWidth;
  315. let canvansHeight = this.imgHeight;
  316. let size = Math.max(canvansWidth, canvansHeight)
  317. let scalc = 1;
  318. if(size > 1365) {
  319. scalc = 1365 / size;
  320. }
  321. this.canvansWidth = canvansWidth * scalc;
  322. this.canvansHeight = canvansHeight * scalc;
  323. this.use2d = use2d;
  324. this.initArea();
  325. const src = val.src || this.imgSrc;
  326. src && this.initImage(src, oldVal === undefined);
  327. },
  328. immediate: true
  329. },
  330. },
  331. methods: {
  332. /** 提供给wxs调用,用来接收图片变更数据 */
  333. dataChange(e) {
  334. // console.log('dataChange', e)
  335. this.scaleWidth = e.width;
  336. this.scaleHeight = e.height;
  337. this.rotate = e.rotate;
  338. this.offsetX = e.x;
  339. this.offsetY = e.y;
  340. },
  341. /** 初始化裁剪区域布局信息 */
  342. initArea() {
  343. // 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
  344. this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
  345. // #ifndef H5
  346. this.sys.windowTop = 0;
  347. this.sys.navigation = true;
  348. // #endif
  349. // #ifdef H5
  350. // h5平台的窗口高度是包含标题栏的
  351. this.sys.windowTop = this.sys.windowTop || 44;
  352. this.sys.navigation = this.navigation;
  353. // #endif
  354. let wp = this.widthPercent;
  355. let hp = this.heightPercent;
  356. if (this.imgWidth > this.imgHeight) {
  357. hp = hp * this.imgHeight / this.imgWidth;
  358. } else if (this.imgWidth < this.imgHeight) {
  359. wp = wp * this.imgWidth / this.imgHeight;
  360. }
  361. const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
  362. const width = size * wp / 100;
  363. const height = size * hp / 100;
  364. const left = (this.sys.windowWidth - width) / 2;
  365. const right = left + width;
  366. const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
  367. const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
  368. this.area = { width, height, left, right, top, bottom };
  369. this.scaleWidth = width;
  370. this.scaleHeight = height;
  371. },
  372. /** 从本地选取图片 */
  373. chooseImage(options) {
  374. // #ifdef MP-WEIXIN || MP-JD
  375. if(uni.chooseMedia) {
  376. uni.chooseMedia({
  377. ...options,
  378. count: 1,
  379. mediaType: ['image'],
  380. success: (res) => {
  381. this.resetData();
  382. this.initImage(res.tempFiles[0].tempFilePath);
  383. }
  384. });
  385. return;
  386. }
  387. // #endif
  388. uni.chooseImage({
  389. ...options,
  390. count: 1,
  391. success: (res) => {
  392. this.resetData();
  393. this.initImage(res.tempFiles[0].path);
  394. }
  395. });
  396. },
  397. /** 重置数据 */
  398. resetData() {
  399. this.imgSrc = '';
  400. this.rotate = 0;
  401. this.offsetX = 0;
  402. this.offsetY = 0;
  403. this.initArea();
  404. },
  405. /**
  406. * 初始化图片信息
  407. * @param {String} url 图片链接
  408. */
  409. initImage(url, isFirst) {
  410. uni.getImageInfo({
  411. src: url,
  412. success: async (res) => {
  413. if (isFirst && this.src === url) await (new Promise((resolve) => setTimeout(resolve, 50)));
  414. this.imgSrc = res.path;
  415. let scale = res.width / res.height;
  416. let areaScale = this.area.width / this.area.height;
  417. if (scale > 1) { // 横向图片
  418. if (scale >= areaScale) { // 图片宽不小于目标宽,则高固定,宽自适应
  419. this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth);
  420. } else { // 否则宽固定、高自适应
  421. this.scaleHeight = res.height * this.scaleWidth / res.width;
  422. }
  423. } else { // 纵向图片
  424. if (scale <= areaScale) { // 图片高不小于目标高,宽固定,高自适应
  425. this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this.scaleHeight / res.height);
  426. } else { // 否则高固定,宽自适应
  427. this.scaleWidth = res.width * this.scaleHeight / res.height;
  428. }
  429. }
  430. // 记录原始宽高,为缩放比列做限制
  431. this.oldWidth = +this.scaleWidth.toFixed(2);
  432. this.oldHeight = +this.scaleHeight.toFixed(2);
  433. },
  434. fail: (err) => {
  435. console.error(err)
  436. }
  437. });
  438. },
  439. /**
  440. * 剪切图片圆角
  441. * @param {Object} ctx canvas 的绘图上下文对象
  442. * @param {Number} radius 圆角半径
  443. * @param {Number} scale 生成图片的实际尺寸与截取区域比
  444. * @param {Function} drawImage 执行剪切时所调用的绘图方法,入参为是否执行了剪切
  445. */
  446. drawClipImage(ctx, radius, scale, drawImage) {
  447. if(radius > 0) {
  448. ctx.save();
  449. ctx.beginPath();
  450. const w = this.canvansWidth;
  451. const h = this.canvansHeight;
  452. if(w === h && radius >= w / 2) { // 圆形
  453. ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
  454. } else { // 圆角矩形
  455. if(w !== h) { // 限制圆角半径不能超过短边的一半
  456. radius = Math.min(w / 2, h / 2, radius);
  457. // radius = Math.min(Math.max(w, h) / 2, radius);
  458. }
  459. ctx.moveTo(radius, 0);
  460. ctx.arcTo(w, 0, w, h, radius);
  461. ctx.arcTo(w, h, 0, h, radius);
  462. ctx.arcTo(0, h, 0, 0, radius);
  463. ctx.arcTo(0, 0, w, 0, radius);
  464. ctx.closePath();
  465. }
  466. ctx.clip();
  467. drawImage && drawImage(true);
  468. ctx.restore();
  469. } else {
  470. drawImage && drawImage(false);
  471. }
  472. },
  473. /**
  474. * 旋转图片
  475. * @param {Object} ctx canvas 的绘图上下文对象
  476. * @param {Number} rotate 旋转角度
  477. * @param {Number} scale 生成图片的实际尺寸与截取区域比
  478. */
  479. drawRotateImage(ctx, rotate, scale) {
  480. if(rotate !== 0) {
  481. // 1. 以图片中心点为旋转中心点
  482. const x = this.scaleWidth * scale / 2;
  483. const y = this.scaleHeight * scale / 2;
  484. ctx.translate(x, y);
  485. // 2. 旋转画布
  486. ctx.rotate(rotate * Math.PI / 180);
  487. // 3. 旋转完画布后恢复设置旋转中心时所做的偏移
  488. ctx.translate(-x, -y);
  489. }
  490. },
  491. drawImage(ctx, image, callback) {
  492. // 生成图片的实际尺寸与截取区域比
  493. const scale = this.canvansWidth / this.area.width;
  494. if(this.backgroundColor) {
  495. if(ctx.setFillStyle) ctx.setFillStyle(this.backgroundColor);
  496. else ctx.fillStyle = this.backgroundColor;
  497. ctx.fillRect(0, 0, this.canvansWidth, this.canvansHeight);
  498. }
  499. this.drawClipImage(ctx, this.radius, scale, () => {
  500. this.drawRotateImage(ctx, this.rotate, scale);
  501. const r = this.rotate / 90;
  502. ctx.drawImage(
  503. image,
  504. [
  505. (this.offsetX - this.area.left),
  506. (this.offsetY - this.area.top),
  507. -(this.offsetX - this.area.left),
  508. -(this.offsetY - this.area.top)
  509. ][r] * scale,
  510. [
  511. (this.offsetY - this.area.top),
  512. -(this.offsetX - this.area.left),
  513. -(this.offsetY - this.area.top),
  514. (this.offsetX - this.area.left)
  515. ][r] * scale,
  516. this.scaleWidth * scale,
  517. this.scaleHeight * scale
  518. );
  519. });
  520. },
  521. /**
  522. * 绘图
  523. * @param {Object} canvas
  524. * @param {Object} ctx canvas 的绘图上下文对象
  525. * @param {String} src 图片路径
  526. * @param {Function} callback 开始绘制时回调
  527. */
  528. draw2DImage(canvas, ctx, src, callback) {
  529. // console.log('draw2DImage', canvas, ctx, src, callback)
  530. if(canvas) {
  531. const image = canvas.createImage();
  532. image.onload = () => {
  533. this.drawImage(ctx, image);
  534. // 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
  535. callback && setTimeout(callback, this.delay);
  536. };
  537. image.onerror = (err) => {
  538. console.error(err)
  539. uni.hideLoading();
  540. };
  541. image.src = src;
  542. } else {
  543. this.drawImage(ctx, src);
  544. setTimeout(() => {
  545. ctx.draw(false, callback);
  546. }, 200);
  547. }
  548. },
  549. /**
  550. * 画布转图片到本地缓存
  551. * @param {Object} canvas
  552. * @param {String} canvasId
  553. */
  554. canvasToTempFilePath(canvas, canvasId) {
  555. // console.log('canvasToTempFilePath', canvas, canvasId)
  556. uni.canvasToTempFilePath({
  557. canvas,
  558. canvasId,
  559. x: 0,
  560. y: 0,
  561. width: this.canvansWidth,
  562. height: this.canvansHeight,
  563. destWidth: this.imgWidth, // 必要,保证生成图片宽度不受设备分辨率影响
  564. destHeight: this.imgHeight, // 必要,保证生成图片高度不受设备分辨率影响
  565. fileType: this.fileType, // 目标文件的类型,默认png
  566. success: (res) => {
  567. // 生成的图片临时文件路径
  568. this.handleImage(res.tempFilePath);
  569. },
  570. fail: (err) => {
  571. uni.hideLoading();
  572. uni.showToast({ title: '裁剪失败,生成图片异常!', icon: 'none' });
  573. }
  574. }, this);
  575. },
  576. /** 确认裁剪 */
  577. cropClick() {
  578. uni.showLoading({ title: '裁剪中...', mask: true });
  579. if(!this.use2d) {
  580. const ctx = uni.createCanvasContext('imgCanvas', this);
  581. ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
  582. this.draw2DImage(null, ctx, this.imgSrc, () => {
  583. this.canvasToTempFilePath(null, 'imgCanvas');
  584. });
  585. return;
  586. }
  587. // #ifdef MP-WEIXIN
  588. const query = uni.createSelectorQuery().in(this);
  589. query.select('#imgCanvas')
  590. .fields({ node: true, size: true })
  591. .exec((res) => {
  592. const canvas = res[0].node;
  593. const dpr = uni.getSystemInfoSync().pixelRatio;
  594. canvas.width = res[0].width * dpr;
  595. canvas.height = res[0].height * dpr;
  596. const ctx = canvas.getContext('2d');
  597. ctx.scale(dpr, dpr);
  598. ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
  599. this.draw2DImage(canvas, ctx, this.imgSrc, () => {
  600. this.canvasToTempFilePath(canvas);
  601. });
  602. });
  603. // #endif
  604. },
  605. handleImage(tempFilePath){
  606. // 在H5平台下,tempFilePath 为 base64
  607. // console.log(tempFilePath)
  608. uni.hideLoading();
  609. this.$emit('crop', { tempFilePath });
  610. }
  611. }
  612. }
  613. </script>
  614. <style lang="scss" scoped>
  615. .image-cropper {
  616. position: fixed;
  617. left: 0;
  618. right: 0;
  619. top: 0;
  620. bottom: 0;
  621. overflow: hidden;
  622. display: flex;
  623. flex-direction: column;
  624. background-color: #000;
  625. .img-canvas {
  626. position: absolute !important;
  627. transform: translateX(-100%);
  628. }
  629. .pic-preview {
  630. width: 100%;
  631. flex: 1;
  632. position: relative;
  633. .crop-mask-block {
  634. background-color: rgba(51, 51, 51, 0.8);
  635. z-index: 2;
  636. position: fixed;
  637. box-sizing: border-box;
  638. pointer-events: none;
  639. }
  640. .crop-circle-box {
  641. position: fixed;
  642. box-sizing: border-box;
  643. z-index: 2;
  644. pointer-events: none;
  645. overflow: hidden;
  646. .crop-circle {
  647. width: 100%;
  648. height: 100%;
  649. }
  650. }
  651. .crop-image {
  652. padding: 0 !important;
  653. margin: 0 !important;
  654. border-radius: 0 !important;
  655. display: block !important;
  656. backface-visibility: hidden;
  657. }
  658. .crop-border {
  659. position: fixed;
  660. border: 1px solid #fff;
  661. box-sizing: border-box;
  662. z-index: 3;
  663. pointer-events: none;
  664. }
  665. .crop-grid {
  666. position: fixed;
  667. z-index: 3;
  668. border-style: dashed;
  669. border-color: #fff;
  670. pointer-events: none;
  671. opacity: 0.5;
  672. }
  673. .crop-angle {
  674. position: fixed;
  675. z-index: 3;
  676. border-style: solid;
  677. border-color: #fff;
  678. pointer-events: none;
  679. }
  680. }
  681. .fixed-bottom {
  682. position: fixed;
  683. left: 0;
  684. right: 0;
  685. bottom: 0;
  686. z-index: 99;
  687. display: flex;
  688. flex-direction: row;
  689. background-color: $uni-bg-color-grey;
  690. .action-bar {
  691. position: absolute;
  692. top: -90rpx;
  693. left: 10rpx;
  694. display: flex;
  695. .rotate-icon {
  696. background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABCFJREFUaEPtml3IpVMUx3//ko/ChTIyiGFSMyhllI8bc4F85yuNC2FCqLmQC1+FZORiEkUMNW7UjKjJULgxV+NzSkxDhEkZgwsyigv119J63p7zvOc8z37OmXdOb51dz82711r7/99r7bXXXucVi3xokeNnRqCvB20fDmwAlgK/5bcD+FTSr33tHXQP2H4MeHQE0A+B5yRtLiUyDQJrgVc6AAaBpyV93kXkoBMIQLbfBS5NcK8BRwDXNcD+AdwnaVMbiWkRCPBBohpxHuK7M7865sclRdgNHVMhkF6IMIpwirFEUhzo8M7lwIvASTXEqyVtH8ZgagQSbOzsDknv18HZXpHn5IL8+94IOUm7miSmSqAttjPdbgGuTrnNktYsGgLpoYuAD2qg1zRTbG8P2D4SOC6/Q7vSHPALsE/S7wWy80RsPw/ckxMfSTq/LtRJwPbxwF3ASiCUTxwHCPAnEBfVF8AWSTtL7Ng+LfWOTfmlkn6udFsJ5K15R6a4kvX6yGyUFBvTOWzHXXFzCt4g6c1OArYj9iIGh43YgR+BvztXh1PSa4cMkd0jaVmXDduPAE+k3HpJD7cSGFKvfAc8FQUX8IOk/V2L1udtB/hTgdOBW4Aba/M7Ja1qs2f7euCNlHlZUlx4/495IWQ7Jl+qGbxX0gt9AHfJ2o6zFBVoNVrDKe+F3Sm8VdK1bQQ+A85JgXckXdkFaJx527cC9TpnVdvBtl3h2iapuhsGPdBw1b9xnUvaNw7AEh3bnwDnpuwGSfeP0rN9NvAMELXRXFkxEEK2nwQeSiOtRVQJwC4Z29cAW1Nuu6TVXTrN+SaBt4ErUug2Sa/2NdhH3vZy4NvU2S/p6D768w5xI3WOrAD7LtISFpGdIhVXKfaYvjd20wP13L9M0p4DBbaFRKToSLExVkr6qs+aIwlI6iwz+izUQqC+ab29PiMwqRcmPXczD8w8MFj1zg7xXEqbpdHCw7FgWSjafZL+KcQxtpjteCeflwYulFR/J3TabSslVkj6utPChAK2f6q9uZdLitKieLQRuExSvX9ZbLRUMFs09efpUZL+KtUfVo1GW/umNHC3pOhRLtiwfSbwZS6wV9IJfRdreuBBYH0a2STp9r4G+8jbXgc8mzoDT8VSO00ClwDv1ZR7XyylC4ec7ejaLUmdsV6Aw7oSbwFXpdFdks7qA6pU1na0aR6owgeIR/1cx63UzjAC0YXYVjMQHlkn6ZtSo21ytuPZGKFagQ/xsXZ/3iGuFrYdjafXG0DiQMeBi47c9/GV3BO247UV38n5o0UAP6xmu7jFOGxjRr66On5NPBDOCBsDTapxjHY1dyOcolNXnYlx1himE53p2PmNkxosevfavhg4Izt2k7TXPwZ2S6p6QZPin/2rwcQ7OKmBohCadJGF1P8PG6aaQBKVX/8AAAAASUVORK5CYII=');
  697. background-size: 60% 60%;
  698. background-repeat: no-repeat;
  699. background-position: center;
  700. width: 80rpx;
  701. height: 80rpx;
  702. &.is-reverse {
  703. transform: rotateY(180deg);
  704. }
  705. }
  706. }
  707. .rechoose {
  708. color: $uni-color-primary;
  709. padding: 0 $uni-spacing-row-lg;
  710. line-height: 100rpx;
  711. }
  712. .choose-btn {
  713. color: $uni-color-primary;
  714. text-align: center;
  715. line-height: 100rpx;
  716. flex: 1;
  717. }
  718. .button {
  719. margin: auto $uni-spacing-row-lg auto auto;
  720. background-color: $uni-color-primary;
  721. color: #fff;
  722. }
  723. }
  724. .safe-area-inset-bottom {
  725. padding-bottom: 0;
  726. padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
  727. padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
  728. }
  729. }
  730. </style>