Skip to content

思路

  1. 一个自适应的canvas画布(图片大小就是canvas画布大小)
  2. 绘制图片 -> drawImage
  3. 蒙层 -> 半透明遮罩层
  4. 截图的矩形位 -> 在蒙层的上方鼠标移动的范围内进行图片在绘制
  5. 获取当前图片截图后的数据
  6. 放入另一个canvas画布中
  7. 保存新的canvas为图片
vue
<script setup>
import { useTemplateRef, computed, ref } from 'vue'

// 使用 useTemplateRef 获取模板中的 canvas 和 container 引用
const canvas = useTemplateRef('canvas')
const container = useTemplateRef('container')

const canvas2 = useTemplateRef('canvas2')
const container2 = useTemplateRef('canvas2Container')

const imageFile = useTemplateRef('imageFile')
// 创建一个 Image 对象用于加载和处理图像
const image = new Image()
// 初始化位置数组,用于存储鼠标按下时的初始位置
const initPos = ref([])
// 用于存储截图数据的响应式变量
let screenShotData = ref([])
// 鼠标按下的状态
const mouseDown = ref(false)
// 定义蒙层的不透明度常量
const MASK_OPACITY = 0.5

// 计算属性,获取 canvas 的 2D 绘图上下文
const ctx = computed(() => {
    return canvas.value.getContext('2d')
})
const ctx2 = computed(() => {
    return canvas2.value.getContext('2d')
})

/**
 * 处理文件选择事件
 */
function handleFileChange(e) {
    const file = e.target.files[0]
    const reader = new FileReader()

    reader.readAsDataURL(file)

    reader.onload = function (e) {
        const data = e.target.result
        image.src = data
        image.onload = function () {
            const { width, height } = image
            // 根据图像尺寸生成画布,并绘制图像和蒙层
            generateCanvas(container, canvas, width, height)
            ctx.value.drawImage(image, 0, 0, width, height)
            drwImageMask(0, 0, width, height, MASK_OPACITY)
        }
    }
}

/**
 * 根据容器和图像尺寸生成画布
 * @param {Object} container - 容器引用
 * @param {Object} canvas - 画布引用
 * @param {Number} width - 画布宽度
 * @param {Number} height - 画布高度
 */
function generateCanvas(container, canvas, width, height) {
    container.value.style.width = width + 'px'
    container.value.style.height = height + 'px'
    canvas.value.width = width
    canvas.value.height = height
    container.value.style.display = 'block'
}

/**
 * 绘制图像蒙层
 * @param {Number} x - 蒙层起始 x 坐标
 * @param {Number} y - 蒙层起始 y 坐标
 * @param {Number} width - 蒙层宽度
 * @param {Number} height - 蒙层高度
 * @param {Number} opacity - 蒙层不透明度
 */
function drwImageMask(x, y, width, height, opacity) {
    ctx.value.fillStyle = `rgba(0,0,0,${opacity})`
    ctx.value.fillRect(x, y, width, height)
}

/**
 * 处理画布鼠标按下事件
 */
function handleCanvasMousedown(e) {
    mouseDown.value = true
    initPos.value = [e.offsetX, e.offsetY]
}

/**
 * 处理画布鼠标移动事件
 */
function handleCanvasMousemove(e) {
    if (mouseDown.value) {
        const endX = e.offsetX
        const endY = e.offsetY
        const [startX, startY] = initPos.value
        const reactWidth = endX - startX
        const reactHeight = endY - startY

        const { width, height } = canvas.value

        // 更新截图数据,并重新绘制画布和蒙层
        screenShotData.value = [startX, startY, reactWidth, reactHeight]
        ctx.value.clearRect(0, 0, width, height)
        drwImageMask(0, 0, width, height, MASK_OPACITY)
        drwScreenShot(width, height, reactWidth, reactHeight)
    }
}

/**
 * 处理画布鼠标抬起事件
 */
function handleCanvasMouseup(e) {
    mouseDown.value = false
    // 根据截图数据绘制最终的截图图像
    drwScreenShotImage(...screenShotData.value)
}

/**
 * 绘制截图区域
 * @param {Number} canWidth - 画布宽度
 * @param {Number} canHeight - 画布高度
 * @param {Number} reactWidth - 截图区域宽度
 * @param {Number} reactHeight - 截图区域高度
 */
function drwScreenShot(canWidth, canHeight, reactWidth, reactHeight) {
    ctx.value.globalCompositeOperation = 'destination-out' // 当前源外侧绘制
    ctx.value.fillStyle = '#000'
    ctx.value.fillRect(...initPos.value, reactWidth, reactHeight)

    ctx.value.globalCompositeOperation = 'destination-over' // 当前源之上绘制
    ctx.value.drawImage(image, 0, 0, canWidth, canHeight)
}

/**
 * 绘制截图图像到第二个画布
 * @param {Number} startX - 截图起始 x 坐标
 * @param {Number} startY - 截图起始 y 坐标
 * @param {Number} reactWidth - 截图宽度
 * @param {Number} reactHeight - 截图高度
 */
function drwScreenShotImage(startX, startY, reactWidth, reactHeight) {
    const data = ctx.value.getImageData(startX, startY, reactWidth, reactHeight)
    generateCanvas(container2, canvas2, reactWidth, reactHeight)
    ctx2.value.clearRect(startX, startY, reactWidth, reactHeight)
    ctx2.value.putImageData(data, 0, 0)
}
</script>

<template>
    <!-- 文件输入控件,用于选择图像文件 -->
    <input ref="imageFile" class="mb-10" type="file" name="" accept="image/*" @change="handleFileChange" />
    <!-- 主画布容器 -->
    <div ref="container" class="w-full hidden border border-style-dashed border-amber">
        <canvas
            ref="canvas"
            @mousedown="handleCanvasMousedown"
            @mousemove="handleCanvasMousemove"
            @mouseup="handleCanvasMouseup"
        ></canvas>
    </div>

    <!-- 截图画布容器 -->
    <div ref="canvas2Container" class="w-full hidden border border-style-dashed border-amber">
        <canvas ref="canvas2"></canvas>
    </div>
</template>