qf-image-cropper.vue 23 KB

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