基于 CropperJS 的图片编辑器实现

在现代 Web 应用中,图片处理是一个常见需求。上篇文章上手 cropperjs v2:一个优雅的图片裁剪解决方案简单介绍了 CropperJS 的图片裁剪方案,本文将介绍如何基于 CropperJS 构建一个功能丰富的图片编辑器,支持裁剪、滤镜、蒙层等功能。

编辑器核心功能

1. 组件结构

vue 复制代码
<template>
    <div class="image-editor">
        <div class="editor-container" :style="containerStyle">
            <cropper-canvas ref="canvas" :background="!showCanvasBg && !bgImage" 
                :initial-width="pos.width" :initial-height="pos.height">
                <cropper-image :style="{ filter: filterAttr }" ref="image" 
                    :scalable="scale" translatable :src="src"></cropper-image>
                
                <cropper-selection initial-coverage="1">
                    <cropper-grid bordered covered></cropper-grid>
                    <cropper-crosshair centered></cropper-crosshair>
                    
                    <!-- 蒙层系统 -->
                    <div v-if="mask" class="editor-mask" 
                        :style="{ backgroundImage: `url(${mask})` }"></div>
                    
                    <component v-if="maskComponent" 
                        :is="maskComponent" 
                        :color="color">
                    </component>
                </cropper-selection>
            </cropper-canvas>
        </div>
        
        <!-- 控制面板 -->```
        <el-form class="img-attr" label-width="auto">
            <el-form-item label="背景色:" v-if="showCanvasBg"><el-color-picker v-model="canvasBg" /></el-form-item>
                <!-- 更多控制项... -->
            </el-form-item>
       </el-form>
            
            <div class="action-buttons">
                <button @click="confirm">确认</button>
                <button @click="reset">重置</button>
                <button @click="cancel">取消</button>
            </div>
        </div>
    </div>
</template>

2. 滤镜系统实现

javascript 复制代码
const filterAttr = computed(() => {
    const blurAttr = props?.editOpt?.blur ? `blur(${blur.value}px)` : ""
    const opaAttr = props?.editOpt?.opa && modeCom.value == 1 ? `opacity(${opa.value / 100})` : ""
    const brightnessAttr = props?.editOpt?.brightness ? `brightness(${brightness.value / 100})` : ""
    const saturationAttr = props?.editOpt?.saturation ? `saturate(${saturation.value / 100})` : ""
    const contrastAttr = props?.editOpt?.contrast ? `contrast(${contrast.value / 100})` : ""
    const grayAttr = props?.editOpt?.gray && gray.value ? `grayscale(${grayscale.value / 100})` : ""
    const hueAttr = props?.editOpt?.hue ? `hue-rotate(${hue.value}deg)` : ""
    const invertAttr = props?.editOpt?.invert ? `invert(${invert.value / 100})` : ""
    const sepiaAttr = props?.editOpt?.sepia ? `sepia(${sepia.value / 100})` : ""
    
    return `${blurAttr} ${opaAttr} ${brightnessAttr} ${saturationAttr} ${contrastAttr} ${grayAttr} ${hueAttr} ${invertAttr} ${sepiaAttr}`
})

3. 双模式处理逻辑

javascript 复制代码
// 裁剪模式处理
const confirmClipMode = async () => {
  const originalCanvas = await canvasRef.value.$toCanvas()
  const offScreenCanvas = document.createElement("canvas")
  offScreenCanvas.width = originalCanvas.width
  offScreenCanvas.height = originalCanvas.height
  const ctx = offScreenCanvas.getContext("2d")
  // 由于需要储存的是jpeg文件,这里根据选择的背景色做下处理,否则默认是个黑底
  ctx.fillStyle = canvasBg.value
  ctx.fillRect(0, 0, offScreenCanvas.width, offScreenCanvas.height)
  ctx.drawImage(originalCanvas, 0, 0)

  const dataURL = offScreenCanvas.toDataURL("image/jpeg")
  return {
    dataURL
  }
}

// 画布模式处理
const confirmCanvasMode = async () => {
    // 获取画布布局
    const canvasElement = canvasRef.value
    const canvasRect = canvasElement.getBoundingClientRect()

    // 获取画布和图片的尺寸
    const imgElement = imageRef.value.querySelector("img") || imageRef.value.children[0] || imageRef.value.shadowRoot.querySelector("img")
    const imgRect = imgElement.getBoundingClientRect()
    const imgWidth = imgElement.naturalWidth
    const imgHeight = imgElement.naturalHeight

    // 图片尺寸
    const scaledWidth = imgRect.width
    const scaledHeight = imgRect.height

    // 最终坐标(相对于画布左上角)
    const x = imgRect.left - canvasRect.left
    const y = imgRect.top - canvasRect.top

    // 生成图片dataURL
    const canvas = document.createElement("canvas")
    canvas.width = scaledWidth
    canvas.height = scaledHeight
    const ctx = canvas.getContext("2d")
    ctx.filter = filterAttr.value
    ctx.drawImage(imgElement, 0, 0, imgWidth, imgHeight, 0, 0, scaledWidth, scaledHeight)

    const dataURL = canvas.toDataURL("image/jpeg")

    return {
        dataURL,
        x: Math.round(x),
        y: Math.round(y),
        width: Math.round(scaledWidth),
        height: Math.round(scaledHeight)
    }
}

与函数式弹窗集成

依照之前文章仿 ElementPlus 的函数式弹窗调用创建弹窗使用。

1. 创建编辑器弹窗

vue 复制代码
<!-- ImageEditorDialog.vue -->
<template>
    <div class="editor-dialog">
        <ImageEditor
            :src="src"
            :pos="pos"
            :options="options"
            @confirm="handleConfirm"
            @cancel="handleCancel"
        />
    </div>
</template>

<script setup>
import ImageEditor from './ImageEditor.vue'

const props = defineProps({
    src: String,
    pos: Object,
    options: Object
})

const emit = defineEmits(['confirm', 'cancel'])

const handleConfirm = (data) => {
    emit('confirm', data)
}

const handleCancel = () => {
    emit('cancel')
}
</script>

2. 函数式调用接口

javascript 复制代码
// imageEditor.js
import { createDialog } from './dialogManager'
import ImageEditorDialog from './ImageEditorDialog.vue'

export async function openImageEditor(src, options = {}) {
    const defaultOptions = {
        pos: { width: 800, height: 600 },
        modeList: ['crop', 'canvas'],
        editOptions: {
            blur: true,
            brightness: true,
            saturation: true
        }
    }
    
    const result = await createDialog(ImageEditorDialog, {
        src,
        ...defaultOptions,
        ...options
    })
    
    return result
}

3. 业务中使用

javascript 复制代码
// 在业务组件中使用
import { openImageEditor } from './imageEditor'

const handleEditImage = async (imageFile) => {
    try {
        const imageUrl = URL.createObjectURL(imageFile)
        
        const result = await openImageEditor(imageUrl, {
            pos: { width: 400, height: 300 },
            editOptions: {
                brightness: true,
                contrast: true
            },
            mask: require('./assets/mask.png')
        })
        
        if (result.action === 'confirm') {
            // 处理编辑后的图片
            await saveEditedImage(result.data)
        }
        
        // 清理资源
        URL.revokeObjectURL(imageUrl)
    } catch (error) {
        console.error('图片编辑失败:', error)
    }
}

总结

这个基于 cropperjs v2 的图片编辑器展示了如何将简单的裁剪功能扩展为全面的图片处理解决方案。通过双模式设计、丰富的滤镜系统、灵活的蒙层支持和优雅的弹窗管理,我们创建了一个既强大又易用的编辑工具。

相关推荐
未来之窗软件服务1 小时前
自己写算法(九)网页数字动画函数——东方仙盟化神期
前端·javascript·算法·仙盟创梦ide·东方仙盟·东方仙盟算法
你的人类朋友2 小时前
什么是断言?
前端·后端·安全
FIN66683 小时前
昂瑞微:实现精准突破,攻坚射频“卡脖子”难题
前端·人工智能·安全·前端框架·信息与通信
椎4953 小时前
苍穹外卖前端nginx错误之一解决
运维·前端·nginx
@。1243 小时前
对于灰度发布(金丝雀发布)的了解
开发语言·前端
我有一棵树3 小时前
前端图片加载失败、 img 出现裂图的原因全解析
前端
FIN66683 小时前
昂瑞微冲刺科创板:硬科技与资本市场的双向奔赴
前端·人工智能·科技·前端框架·智能
im_AMBER3 小时前
杂记 14
前端·笔记·学习·web
牧杉-惊蛰3 小时前
disable-devtool 网络安全 禁止打开控制台
前端·css·vue.js
C+ 安口木3 小时前
vue中监听window某个属性被添加或值的变化
前端·javascript·vue.js