很多很多年前,网页端图片裁切都会想到 cropper.js 。到了当前, cropper.js 还是值得一选。这个是基于 js,ts 的插件,不局限你使用 vue 还是 使用 react 或其他的。反正,还是很好用。当下 2.0 版本更好用。
官网: https://fengyuanchen.github.io/cropperjs/v2/zh/
演练场:https://fengyuanchen.github.io/cropperjs/v2/zh/playground.html
这个插件越来越自由,也越来越集中。它只关注输入(img dom)和输出(canvas)[转换成 base64 可能会存在跨域问题]。大只分以下几个模块(每个模块都可以单独使用)。
Cropper
CropperCanvas
CropperImage
CropperSelection
CropperShade
CropperGrid
CropperViewer
# 等等
这些,都各自有各自的渲染职责。加上就有,配置就改变。不加则没有。想加就加,想减就减。是真的好用。 先看一个我做的一个 demo。是基于vue3 + ts 实现的。(不对。里边还加了一些 element-plus 的组件)
构建的时候,也很方便。使用其接口就可以。更好的是是基于组件的方式来处理。最终的目的是什么,是要渲染到 dom 中,让人看到。 组件不就是最直接的方式吗。这个时候,你可以很方便的使用 css 去美化你的产品。只需要少量关注一些配置就好。比如裁切比例,覆盖比列等。
代码
组件:
<template>
<el-dialog v-model="dialog.visible" :width="dialog.width" :title="title" :show-close="false">
<img id="img" :src="props.path" />
<div class="cropper-tools">
<div>
<div class="input-btn">
<label class="el-button el-button--info el-button--small" for="file">选择图片</label>
<input class="hidden" id="file" name="file" type="file" @change="chageImage" />
</div>
<el-button size="small" type="info" @click="actionHandler('rl')">向左旋转</el-button>
<el-button size="small" type="info" @click="actionHandler('rr')">向右旋转</el-button>
<el-button size="small" type="info" @click="actionHandler('fx')">左右镜像</el-button>
<el-button size="small" type="info" @click="actionHandler('fy')">上下镜像</el-button>
</div>
<el-button type="primary" @click="cropperComplete">保存</el-button>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick, watch } from 'vue'
import Cropper, { CropperImage, CropperSelection } from 'cropperjs';
interface Emits {
(e: 'onCropperComplete', value: string):void
}
interface Props {
title: string,
path: string,
outWidth?: number,
conWidth?: number,
conHeight?: number,
aspectRatio?: number,
coverage?: number,
movable?: boolean
rounded?: boolean
}
const emits = defineEmits<Emits>()
const props = withDefaults(defineProps<Props>(), {
title: '',
path: '',
conWidth: 620,
conHeight: 300,
outWidth: 240,
aspectRatio: 1,
coverage: 0.72,
movable: false,
rounded: false
})
const dialog = reactive({
visible: false,
width: '50%'
})
watch(() => props, () => {
dialog.width = (props.conWidth + 208) + 'px'
}, {
immediate: true
})
const cropper = ref<Cropper>()
const open = function() {
dialog.visible = true
initCropper()
}
const close = function() {
dialog.visible = false
}
function initCropper() {
const coverage = Math.round(100 * props.outWidth / Math.min(props.conWidth, props.conHeight)) * 0.01
const viewBorderRadius = props.rounded ? '50%' : '8px';
nextTick(() => {
const template:string = `
<div style="display:flex;width:${props.conWidth + 16}px">
<div>
<cropper-canvas background style="display:flex;border-radius:8px;width:${props.conWidth}px;height:${props.conHeight}px">
<cropper-image></cropper-image>
<cropper-shade hidden></cropper-shade>
<cropper-handle action="move" plain></cropper-handle>
<cropper-selection id="cropperSelection" initial-coverage="${ coverage }" aspect-ratio="${props.aspectRatio}" movable="${props.movable}">
<cropper-grid role="grid" bordered covered></cropper-grid>
<cropper-crosshair theme-color="rgba(238, 238, 238, 0.5)" centered></cropper-crosshair>
<cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
<cropper-handle action="n-resize"></cropper-handle>
<cropper-handle action="e-resize"></cropper-handle>
<cropper-handle action="s-resize"></cropper-handle>
<cropper-handle action="w-resize"></cropper-handle>
<cropper-handle action="ne-resize"></cropper-handle>
<cropper-handle action="nw-resize"></cropper-handle>
<cropper-handle action="se-resize"></cropper-handle>
<cropper-handle action="sw-resize"></cropper-handle>
</cropper-selection>
</cropper-canvas>
</div>
<div style="width:160px;margin-left:16px">
<div class="cropper-viewers">
<cropper-viewer selection="#cropperSelection" style="width:160px;border-radius:${viewBorderRadius};"></cropper-viewer>
</div>
</div>
</div>
`
if (!cropper.value) {
try {
document.getElementById('img')?.setAttribute('crossOrigin', 'anonymous')
} catch(error) {
console.log('img set crossOrigin fail')
}
cropper.value = new Cropper(document.getElementById('img') as HTMLImageElement, {template})
}
})
}
function chageImage() {
const fileInput: HTMLInputElement = document.getElementById('file') as HTMLInputElement
const files:FileList = fileInput?.files as FileList
if (files.length > 0 ) {
if (cropper.value) {
const cropperImage:CropperImage | null = cropper.value.getCropperImage()
if (cropperImage) {
cropperImage.setAttribute('src', window.URL.createObjectURL(files[0]))
}
}
}
}
function actionHandler(value:string) {
if (!cropper.value) {
return
}
switch (value) {
case 'rl':
cropper.value?.getCropperImage()?.$rotate('-30deg')
break;
case 'rr':
cropper.value?.getCropperImage()?.$rotate('30deg')
break;
case 'fx':
cropper.value?.getCropperImage()?.$scale(-1, 1)
break;
case 'fy':
cropper.value?.getCropperImage()?.$scale(1, -1)
break;
default:
break;
}
}
function cropperComplete() {
const cropperSelection:CropperSelection | undefined | null = cropper.value?.getCropperSelection()
if (cropperSelection) {
const data:Promise<HTMLCanvasElement> = cropperSelection.$toCanvas()
data.then((value) => {
const base:string = value.toDataURL('image/jpeg')
emits('onCropperComplete', base)
close()
})
}
}
defineExpose({
open,
close
})
</script>
<style>
.cropper-tools {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
}
.cropper-tools .input-btn {
display: inline-block;
margin-right: 12px;
}
.hidden {
display: none;
}
.el-dialog {
border-radius: 10px;
}
.el-dialog__header {
padding-top: 12px !important;
padding-bottom: 12px !important;
padding-left: 16px !important;
padding-right: 16px !important;
margin: 0 !important;
}
.el-dialog__body {
padding-left: 16px !important;
padding-right: 16px !important;
padding-top: 12px !important;
padding-bottom: 12px !important;
}
</style>
使用:
<template>
<div class="flex flex-col">
<el-button type="primary" @click="openCropper">打开裁切</el-button>
<img :src="source" class="mt-4" :width="outWidth" />
<span class="text-sm mt-4 break-words">{{ source }}</span>
</div>
<ImageCropper ref="cropper" :path="url" :title="'编辑头像'" :outWidth="outWidth" @onCropperComplete="onCropperComplete"></ImageCropper>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const cropper = ref(null)
const outWidth = ref(240)
const url = 'https://www.yuepaibao.com/storage/upload/image/meet/20230702/1365_20230702065630jUV1Sq_l.jpg'
const source = ref('')
function openCropper() {
cropper.value?.open()
}
function onCropperComplete(value:string) {
source.value = value
}
</script>