Write the Code. Change the World.

8月 23

很多很多年前,网页端图片裁切都会想到 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>

完整代码

https://github.com/vinistudy/cropperjs2.0

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注