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

相关推荐
叫我詹躲躲5 小时前
3 分钟掌握前端 IndexedDB 高效用法,告别本地存储焦虑
前端·indexeddb
默默地离开5 小时前
React Native 入门实战:样式、状态管理与网络请求全解析 (二)
前端·react native
Q_Q5110082855 小时前
python+springboot+vue的旅游门票信息系统web
前端·spring boot·python·django·flask·node.js·php
Q_Q5110082855 小时前
python+django/flask的宠物救助及领养系统javaweb
vue.js·spring boot·python·django·flask·node.js
墨白曦煜5 小时前
快速学习Python(有其他语言基础)
前端·python·学习
工业互联网专业5 小时前
南京某高校校园外卖点餐系统_django
vue.js·python·django·毕业设计·源码·课程设计·校园外卖点餐系统
FserSuN5 小时前
React 标准 SPA 项目 入门学习记录
前端·学习·react.js
YAY_tyy5 小时前
【Cesium 开发实战教程】第六篇:三维模型高级交互:点击查询、材质修改与动画控制
前端·javascript·3d·教程·cesium
蜡笔小电芯5 小时前
【HTML】 第一章:HTML 基础
前端·html