这个裁剪方法可以裁剪圆形、矩形,可以二开,放心食用

先看效果图

矩形的:

小程序之图片裁剪_uniapp

圆形的:

小程序之图片裁剪_小程序_02


具体的方法

组件:

// cropper--index.vue
<template>
  <view>
    <canvas class="fyj_canvas" canvas-id="myCanvas" :style="{width:'100%', height:canvasHeight+'px',}"/>
    <movable-area class="fyj_movable_area text-center hidden" :style="{width:'100%', height:canvasHeight+'px',}">
      <movable-view v-if="src" :style="{width:cutWidth +'px', height:cutHeight + 'px'}" class="fyj_movable_view" :x="x"
                    :y="y" direction="all" :scale="true" @change="movableChange" @scale="handleScale"></movable-view>
      <image class="fyj_photo" id="fyj_photo" :src="src" mode="widthFix"/>
    </movable-area>
    <!--</canvas>-->
    <view style="margin-top:20rpx; padding:0 20rpx;">
      <button class="pull-left" type="warn" size="mini" @click="getPhoto">选择照片/拍照</button>
      <button class="pull-right" type="primary" size="mini" @click="cut">裁剪</button>
      <view class="clearfix"></view>
    </view>
  </view>
</template>

<script setup>
import {ref, reactive, watch, nextTick, getCurrentInstance} from 'vue'

const {aspectRatio} = defineProps({
  // 这里定义了innerText属性,属性值可以在组件使用时指定
  //宽高比 TODO 这个要求裁剪框是矩形的,如果裁剪框是圆形的,这个属性宽高比一定得是1
  aspectRatio: {
    type: Number,
    default: 5 / 7,
  },
})

const screenWidth = ref(uni.getSystemInfoSync().windowWidth)
const canvasHeight = ref(300)
const x = ref(0)
const y = ref(0)
const src = ref('')
const cut_src = ref('')
const cutWidth = ref(0)
const cutHeight = ref(0)

const tempImage = ref('')

const {proxy} = getCurrentInstance()

const emits = defineEmits(['getTempFilePath'])
// 这里是一个自定义方法
//选择照片
const getPhoto = () => {
  const ctx = uni.createCanvasContext('myCanvas', proxy)
  let obj = uni.createSelectorQuery();
  uni.chooseImage({
    count: 1,
    sizeType: ['original', 'compressed'],
    sourceType: ['album', 'camera'],
    success(res) {
      //清空之前的剪切图
      emits('getTempFilePath', {cut_src: '', cutWidth: cutWidth.value, cutHeight: cutHeight.value})

      // tempFilePath可以作为img标签的src属性显示图片
      const tempFilePaths = res.tempFilePaths[0];
      src.value = tempFilePaths
      // 这个变量是为了下面圆形裁剪框使用的 可以结合一个配置项判断是否要使用
      tempImage.value = tempFilePaths
      cut_src.value = ''

      setTimeout(function () {
        // TODO 这里也可以换成小程序获取图片信息的那个api方法
        uni.createSelectorQuery().in(proxy).select('#fyj_photo').boundingClientRect(function (rect) {
          console.log(rect);
          console.log('图片的信息', rect.height);
          canvasHeight.value = rect.height
          setCut();
          // TODO 矩形裁剪框使用的是这个
          // ctx.drawImage(tempFilePaths, 0, 0, screenWidth.value, canvasHeight.value)
          // ctx.draw()

          // 放到上面去了,防止一开始拿不到
          // setCut();
          //确保不同大小的图片,切图不会变形
          x.value = 0
          y.value = 0
        }).exec()
      }, 100)
    }
  })
}

//获取图片高度 暂时没用到
// const getHeight = () => {
//   const query = uni.createSelectorQuery().in(proxy)
//   query.selectAll('#fyj_photo').boundingClientRect()
//   query.exec(function (rect) {
//     console.log(rect);
//     console.log(rect[0].height);
//     canvasHeight.value = rect[0].height
//     // TODO ...
//     ctx.drawImage(tempFilePaths[0], 0, 0, screenWidth.value, canvasHeight.value)
//     ctx.draw();
//     setCut();
//   })
// }

//裁剪框移动事件
const movableChange = debounce((e) => {
  console.log('裁剪框移动', e);
  x.value = e.detail.x
  y.value = e.detail.y


  // TODO 这部分可以抽成一个方法配合一个配置项 圆形裁剪框
  const ctx = uni.createCanvasContext('myCanvas', proxy)
  setTimeout(function () {
    // TODO 这里也可以换成小程序获取图片信息的那个api方法
    uni.createSelectorQuery().in(proxy).select('#fyj_photo').boundingClientRect(function (rect) {
      console.log(rect);
      console.log('图片的信息', rect.height);
      canvasHeight.value = rect.height
      setCut();


      // TODO 圆形裁剪框 裁剪圆形的图片
      // 开始一个新的路径
      ctx.save()
      ctx.beginPath();
      // 创建一个圆形路径
      ctx.arc((x.value + (cutWidth.value / 2)), (y.value + (cutWidth.value / 2)), cutWidth.value / 2, 0, Math.PI * 2, false);
      // 设置当前路径为剪切路径
      ctx.clip();

      // 再次绘制图片,只有在剪切路径内的部分会被绘制
      ctx.drawImage(tempImage.value, 0, 0, screenWidth.value, canvasHeight.value);
      ctx.restore();
      ctx.draw();

      // 这个放到上面去了,防止一开始拿不到
      // setCut();
      // TODO 圆形裁剪框时是不需要的  确保不同大小的图片,切图不会变形
      // x.value = 0
      // y.value = 0
      // TODO 防止第二次圆形绘制失效
      ctx.clearRect(0, 0, screenWidth.value, canvasHeight.value)
    }).exec()
  }, 100)
}, 500)

//截图
const cut = () => {
  console.log(cutHeight.value);
  uni.canvasToTempFilePath({
    // TODO 不管是绘制圆形还是矩形,这里的配置都是一样的
    x: x.value,
    y: y.value,
    width: cutWidth.value,
    height: cutHeight.value,
    destWidth: cutWidth.value,
    destHeight: cutHeight.value,
    canvasId: 'myCanvas',
    success(res) {
      console.log(res.tempFilePath);
      cut_src.value = res.tempFilePath
      emits('getTempFilePath', {cut_src: cut_src.value, cutWidth: cutWidth.value, cutHeight: cutHeight.value})
    }
  }, proxy)
}

//动态设置裁剪框大小,确定高度不得超过canvas的高度
const setCut = () => {
  // TODO 这里比较重要
  cutWidth.value = uni.getSystemInfoSync().windowWidth * 0.8
  cutHeight.value = uni.getSystemInfoSync().windowWidth * 0.8 / aspectRatio

  if (cutHeight.value - 4 > canvasHeight.value) {
    console.log(cutHeight.value);
    console.log(canvasHeight.value);
    cutHeight.value = canvasHeight.value - 4
    cutWidth.value = (canvasHeight.value - 4) * aspectRatio
  } else {
    cutWidth.value = uni.getSystemInfoSync().windowWidth * 0.8
    cutHeight.value = uni.getSystemInfoSync().windowWidth * 0.8 / aspectRatio
  }
  console.log('裁剪框的宽', cutWidth.value);
  console.log('裁剪框的高', cutHeight.value);
}

// 裁剪框缩放事件
const handleScale = debounce((e) => {
  console.log('裁剪框缩放', e);
  cutWidth.value = cutWidth.value * e.detail.scale
  cutHeight.value = cutWidth.value * e.detail.scale
}, 500)


// 防抖
function debounce(func, wait, immediate=false) {
  let timeout;

  return function executedFunction() {
    const context = this;
    const args = arguments;

    const later = function() {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };

    const callNow = immediate && !timeout;

    clearTimeout(timeout);

    timeout = setTimeout(later, wait);

    if (callNow) func.apply(context, args);
  };
}

</script>


<style scoped lang="scss">
// TODO 隐藏画布可以吧注释放开
.fyj_canvas {
  //position: absolute;
  //left: 0;
  //top: -71vh;
  //z-index: -1;
  //opacity: 0;
}

.fyj_movable_area {
  width: 100%;
  height: auto;
  position: relative;
  background: rgba(0, 0, 0, 0.3);
  z-index: 99;
}

.fyj_movable_view {
  border: 2px dashed red;
  // TODO 圆形裁剪框时使用的
  border-radius: 50%;
}

.fyj_photo {
  width: 100%;
}

.fyj_footer {
  margin-top: 20rpx 0;
}

.fyj_footerBtn {
  width: 100%;
  display: inline-block;
  color: #fff;
  border-radius: 0;
  font-size: 32rpx;
}

.fyj_sure {
  background: #fc6b47;
}

.pull-left {
  float: left;
}

.pull-right {
  float: right;
}

.clearfix {
  clear: both;
}

.text-center {
  text-align: center;
}
</style>

使用组件:

// testCropper--index.vue
<template>
  <view>
    <!-- aspectRatio 剪裁图片的宽高比 -->
    <cropper :aspectRatio="1" @getTempFilePath="getCutsrc"></cropper>
    <view v-if="cut_src" class="fyj_cutDiv text-center">
      <image :style="{width: cutWidth + 'px', height: cutHeight + 'px'}" class="fyj_cut_photo" :src="cut_src" mode="widthFix"/>
    </view>
    <view v-if="cut_src" class="fyj_footer text-center">
      <button class="fyj_footerBtn fyj_sure" @click='sure'>确定</button>
    </view>
  </view>
</template>

<script setup>
import {ref} from 'vue'
import cropper from '../components/cropper/index.vue'

const cut_src = ref('');
const cutWidth = ref(0);
const cutHeight = ref(0);

const getCutsrc = (e) => {
  console.log('子组件的传值', e)
  cut_src.value = e.cut_src;
  cutWidth.value = e.cutWidth;
  cutHeight.value = e.cutHeight;
}

</script>


<style scoped lang="scss">
.fyj_footer {
  margin-top: 20rpx 0;
}

.fyj_footerBtn {
  width: 100%;
  display: inline-block;
  color: #fff;
  border-radius: 0;
  font-size: 32rpx;
}

.fyj_sure {
  background: #fc6b47;
}

.fyj_cutDiv {
  margin: 20rpx 0;
}
</style>

参考:

链接1

链接2