实现一个基于react-easy-crop的可自定义截图比例与输出图片像素值的截图组件

裁切组件需求

  1. 裁切缩放的时候,裁切框不可以超出图片边界(调整最小缩放值)

    未设置最小缩放值:
    image

    设置最小缩放值:
    image

  2. 图片初始化展现在裁切组件中的时候,需要利用好组件空间,最大限度的展示图片(调整初始缩放值)

    初始缩放值固定为1,可能无法完全展示图片:
    image

    调整缩放值,尽可能完全展示图片:
    image

    但是有些长宽比过于奇怪的图片,要加以限制,因为展示完全的话,就会违反需求1,超出图片边界:
    image

  3. 裁切出来的图片需要指定宽高(使用canvas的api实现)

  4. 裁切框的比例可自定义、组件宽高可自定义、可以接受任何宽高比例的图片
    image

代码实现

父组件cropView.js:

// cropView.jsx
import { useState, useMemo } from "react";
import ImgCropper from "./imgCropper";

function App() {
  // 初始url
  const url = require("./yigehezi.png");
  // 被剪裁的图像url
  const [croppedUrl, setCroppedUrl] = useState("");
  // 裁切框的宽高
  const cropSize = useMemo(() => ({ width: 280, height: 210 }), []); // 防止触发useEffect
  // "选中框"中,宽、高分别有多少像素
  let croppedAreaPixels = {};

  const handleOk = () => {
    // 点击确定裁剪图片
    let image = new Image();
    image.src = url;
    image.onload = async () => {
      // 使用canvas裁剪图片
      const cvs = document.createElement("canvas");
      const ctx = cvs.getContext("2d");
      const sx = croppedAreaPixels.x;
      const sy = croppedAreaPixels.y;
      const sWidth = croppedAreaPixels.width;
      const sHeight = croppedAreaPixels.height;

      cvs.width = cropSize.width; // 可以自定义成自己想要的截图的横向像素
      cvs.height = cropSize.height; // 可以自定义成自己想要的截图的纵向像素
      cvs.crossOrigin = "Anonymous";
      ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, cvs.width, cvs.height);

      // 生成裁剪后的图片dataUrl以及file对象
      const newImageUrl = cvs.toDataURL("image/jpeg");
      const newImageFile = dataURLtoFile(newImageUrl); // 可以使用newImageFile上传文件
      setCroppedUrl(newImageUrl);
    };

    function dataURLtoFile(dataurl) {
      var arr = dataurl.split(",");
      var mime = arr[0].match(/:(.*?);/)[1];
      var bstr = atob(arr[1]);
      var n = bstr.length;
      var u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      const blob = new Blob([u8arr], { type: mime });
      return new File([blob], "file", { type: "image/jpeg" });
    }
  };

  return (
    <div style={{ backgroundColor: "aqua", width: "800px", height: "800px" }}>
      <button onClick={handleOk}>裁切</button>

      <ImgCropper
        clipingImgUrl={url}
        imgCropperSize={{ width: 400, height: 600 }}
        cropSize={cropSize}
        maxZoom={4}
        onClipOk={(data) => {
          croppedAreaPixels = data;
        }}
        style={{ marginTop: 20 }}
        zoomSpeed={0.15}
      />

      <img src={croppedUrl} />
    </div>
  );
}

export default App;

子组件imgCropper:

// imgCropper.jsx
import { useEffect, useState, useCallback } from 'react'
import Cropper from 'react-easy-crop'

/* 注意:该组件在模态框Modal组件内使用时,需要禁用模态框动画效果,否则此组件会出现异常 */
export default function ImgCropper(props) {
  const {
    /* 必填参数 */
    // 需要被裁切的图片地址
    clipingImgUrl,
    // 组件的宽高({width:xxx,height:xxx})
    imgCropperSize,
    // "裁切框"的宽高({width:xxx,height:xxx})
    cropSize,

    /* 可选参数 */
    // 裁剪结束后的回调函数
    onClipOk,
    // 其他样式
    style = {},
    // 图片最大缩放系数
    maxZoom,
    // 图片缩放速度
    zoomSpeed,
  } = props

  const [crop, setCrop] = useState({ x: 0, y: 0 }) // 图片的位置
  const [zoom, setZoom] = useState(1) // 当前缩放系数
  const [minZoom, setMinZoom] = useState(null) // 最小缩放系数
  const [imageSize, setImageSize] = useState({}) // 当前图片宽高

  // 获取图片的宽高(判断图片的横竖填充 | 设置最小缩放比例)
  const getImageInfo = useCallback(() => {
    if (clipingImgUrl) {
      const image = new Image()
      image.src = clipingImgUrl
      image.onload = () => {
        const { width, height } = image
        setImageSize({
          width,
          height,
        })
      }
    }
  }, [clipingImgUrl])

  useEffect(getImageInfo, [clipingImgUrl])

  useEffect(() => {
    // 裁切范围切换时,重置图片缩放
    setCrop({ x: 0, y: 0 })

    // 裁切范围切换时,重新计算最小缩放值
    if (!Object.keys(imageSize).length) return
    const { width: imageW, height: imageH } = imageSize
    const { width: cropW, height: cropH } = cropSize
    const { width: imgCropperW, height: imgCropperH } = imgCropperSize
    let _minZoom = null

    if (cropW < cropH) {
      _minZoom = Math.max(cropW / ((imageW / imageH) * imgCropperH), cropH / imgCropperH)
    } else {
      _minZoom = Math.max(cropW / imgCropperW, cropH / ((imageH / imageW) * imgCropperW))
    }

    // 1表示刚好放下宽的缩放
    let _zoom = Math.max(_minZoom, Math.min(1, imgCropperH / ((imgCropperW / imageW) * imageH)))
    
    setMinZoom(_minZoom)
    setZoom(_zoom)
  }, [cropSize, imageSize])

  return (
    <div
      style={{
        width: imgCropperSize.width,
        height: imgCropperSize.height,
        position: 'relative',
        ...style,
      }}
    >
      {minZoom && (
        <Cropper
          image={clipingImgUrl} // 图片地址
          crop={crop} // 图片位置
          zoom={zoom}
          minZoom={minZoom || 1}
          maxZoom={maxZoom || 3}
          zoomSpeed={zoomSpeed || 0.2}
          restrictPosition
          objectFit={cropSize.width < cropSize.height ? 'vertical-cover' : 'horizontal-cover'}
          cropSize={cropSize}
          onCropChange={(crop) => {
            // 拖拽、缩放图片后的回调
            setCrop(crop)
          }}
          onCropComplete={(_, croppedAreaPixels) => {
            // 裁切结束后的回调(停止缩放或滚轮停止滚动)
            onClipOk?.({ ...croppedAreaPixels, zoom })
          }}
          onZoomChange={(zoom) => {
            // 使用滚轮缩放后的回调
            setZoom(zoom)
          }}
        />
      )}
    </div>
  )
}

重点分析

最小缩放值与初始缩放值处理

imgCropper.jsx文件中,使用了如下变量进行计算,只需要知道zoom为1的时候,react-easy-crop会将图片变形缩放图片的宽度与裁切框的宽度相同即可,剩下的就是计算得出结果就行,类似于算法题或者应用题。

需要注意的是imageSize值存放的是图片的真实像素值,而非图中变形之后的宽高。
image

计算过程如下:

const { width: imageW, height: imageH } = imageSize
const { width: cropW, height: cropH } = cropSize
const { width: imgCropperW, height: imgCropperH } = imgCropperSize
let _minZoom = null

if (cropW < cropH) {
  _minZoom = Math.max(cropW / ((imageW / imageH) * imgCropperH), cropH / imgCropperH)
} else {
  _minZoom = Math.max(cropW / imgCropperW, cropH / ((imageH / imageW) * imgCropperW))
}

// 1表示刚好放下宽的缩放
let _zoom = Math.max(_minZoom, Math.min(1, imgCropperH / ((imgCropperW / imageW) * imageH)))

setMinZoom(_minZoom)
setZoom(_zoom)

canvas生成截图图片

react-easy-crop并不会帮助我们生成图片,只会给出坐标和宽高,我们需要根据坐标和宽高,结合canvas的API自行生成截图图片。以下是对应代码:

const cvs = document.createElement('canvas')
const ctx = cvs.getContext('2d')
const sx = croppedAreaPixels.x
const sy = croppedAreaPixels.y
const sWidth = croppedAreaPixels.width
const sHeight = croppedAreaPixels.height

cvs.width = cropSize.width // 可以自定义成自己想要的截图的横向像素
cvs.height = cropSize.height // 可以自定义成自己想要的截图的纵向像素
cvs.crossOrigin = "Anonymous";
ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, cvs.width, cvs.height)

根据完整代码可知croppedAreaPixelsd对象就是react-easy-crop返回的坐标与宽高数据对象,数据含义如下:

  1. croppedAreaPixels.xcroppedAreaPixels.y:裁切框的左上角相对于图片真实像素左上角的偏移量
  2. croppedAreaPixels.widthcroppedAreaPixels.height:裁切框的宽高投射到图片上,对应图片真实像素的宽高
  3. drawImage方法的参数及含义如下:drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)
    1. img:需要插入的img/video/canvas 元素。
    2. dx/dy:canvas的开始绘制点位。
    3. dw/dh:canvas的绘制区域大小。
    4. sx/sy:image的开始绘制点位。
    5. sw/wh:image的绘制区域大小。
      image
posted @ 2024-12-19 15:52  CatCatcher  阅读(381)  评论(0)    收藏  举报
#