react实现leaferjs编辑器之形状裁剪功能点

图像编辑器一大功能便是裁切,介于之前公司里面做过一个简单的编辑器,直接套用那个编辑器的裁切功能,而那个裁切重要的是要能够异形(各种稀奇古怪的形状)

查询leaferjs 文档关于裁切部分,并实验demo,基本的裁切可以这样使用,我这里用的裁切图是一个黑色圆形png

ts 复制代码
const group = new Group();

// 创建要被裁剪的图片
const targetImage = new Image({
  name: this.cropImageName,
  url: imageSrc,
  width: 200,
  height: 200,
});

// 创建遮罩图片(形状)
const maskImage = new Image({
  name: this.cropMaskImageName,
  url: maskSrc,
  x: 100,
  y: 100,
  width: 100,
  height: 100,
  mask: true,
});

// 添加到组中
group.add([maskImage, targetImage]);

这里可以说是最终要呈现的样子,进行交互时发现我们拖拽移动的是targetImage,实际应该是maskImage和target同时移动,同样是查阅文档发现只需要给group的draggable和editable设置为true就可以了

ts 复制代码
const group = new Group({
  draggable: true,
  editable: true,
});

给group添加了draggable和editable为true之后,操作裁剪的区域就可以是maskImage和targetImage进行同样的操作

那么问题来了,如果targetImage这两个属性也设置成true会怎么样呢?冲突是什么效果?

测试后发现group的交互效果消失了,那么现在已经可以有一个大体的方向了(后面会发现此方向因为暂时对leaferjs了解的不够会很困难):整个裁剪功能的交互便是group与其子元素的参数不断地对应修改。

功能具体脉络基本确定,并且通过实验确定不断修改能够达成裁剪不同的区域

但是这里有一个问题,maskImage裁剪区域外在Ui界面上是看不到的,如果在动手裁剪时,肯定是裁剪区域和整个图像都能看到并泾渭分明才是友好交互。

但,博主左右想了想,又翻了翻文档暂时没找到比较好的舒服的方法,于是综合考虑,决定再次封装一个与编辑器和leaferjs无相关的组件用以确定遮罩。

css 复制代码
.editImg {
    position: absolute;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

.editImgMask {
    position: absolute;
    width: 100%;
    height: 100%;
    background: #000000;
    opacity: 0.5;
}
ts 复制代码
import { ModalPageInstance } from '@/abstractClass/ModalPageInstance';
import { ConstructorParamsBase } from '@/abstractClass/PageInstance';
import { DialogPageInstance } from '@/abstractClass/DialogPageInstance';
import { MediaUtils } from '@/utils/MediaUtils';

interface ImgConfig {
  src: string
  naturalWidth: number,
  naturalHeight: number,
  width: number,
  height: number,
}

interface MaskConfig {
  src: string
  naturalWidth: number,
  naturalHeight: number,
  width: number,
  height: number,
  offsetX: number,
  offsetY: number,
}

interface State {
  imgConfig: Partial<ImgConfig>,
  maskConfig: Partial<MaskConfig>,
}

const maxWidth = 500;
const maxHeight = 500;

export class ProfiledModalInstance extends ModalPageInstance<State> {
  constructor({ forceUpdate, state }: ConstructorParamsBase<State>) {
    super();
    this.setForceUpdate(forceUpdate);
    this.setState({
      ...state,
    });

    this.initImageConfigPromise()
      .then(() => this.initMaskConfigPromise())
      .then(() => {
        this.setLoading(false);
      });
  }

  async initImageConfigPromise() {
    const { imgConfig } = this.state;
    const imageDom = await MediaUtils.imageOnloadPromise(imgConfig.src);

    imgConfig.naturalHeight = imageDom.naturalHeight;
    imgConfig.naturalWidth = imageDom.naturalWidth;
    imgConfig.width = imageDom.naturalWidth;
    imgConfig.height = imageDom.naturalHeight;
    imageDom.remove();

    const widthScale = maxWidth / imgConfig.width;
    const heightScale = maxHeight / imgConfig.height;

    const resultScale = widthScale < heightScale ? widthScale : heightScale;

    imgConfig.width *= resultScale;
    imgConfig.height *= resultScale;
  }

  async initMaskConfigPromise() {
    const { maskConfig, imgConfig } = this.state;

    const imageDom = await MediaUtils.imageOnloadPromise(maskConfig.src);

    maskConfig.naturalHeight = imageDom.naturalHeight;
    maskConfig.naturalWidth = imageDom.naturalWidth;
    maskConfig.width = imageDom.naturalWidth;
    maskConfig.height = imageDom.naturalHeight;
    imageDom.remove();

    const widthScale = imgConfig.width / maskConfig.width;
    const heightScale = imgConfig.height / maskConfig.height;

    const resultScale = widthScale < heightScale ? widthScale : heightScale;

    maskConfig.width *= resultScale;
    maskConfig.height *= resultScale;

    maskConfig.offsetX = (imgConfig.width - maskConfig.width) / 2;
    maskConfig.offsetY = (imgConfig.height - maskConfig.height) / 2;
  }

  getInfo() {
    const { imgConfig, maskConfig } = this.state;
    const imageScale = imgConfig.naturalWidth / imgConfig.width;
    return {
      maskWidth: maskConfig.width * imageScale,
      maskHeight: maskConfig.height * imageScale,
      offsetX: maskConfig.offsetX * imageScale,
      offsetY: maskConfig.offsetY * imageScale,
    }
  }
}
ts 复制代码
/* eslint-disable jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions,jsx-a11y/no-noninteractive-element-interactions */
import React, { FC } from 'react';
import { Slider, Spin } from 'antd';
import { useCreation, useUpdate } from 'ahooks';
import { css, cx } from '@emotion/css';
import styles from './ProfiledModal.css';
import { ProfiledModalInstance } from './ProfiledModalInstance';
import { InstructionMountProps } from '@/hooks/useElementsContextHolder';
import { appInstance } from '@/runTime';
import { EmotionBaseInstance } from '@/abstractClass/EmotionBaseInstance';

const ProfiledModal: FC<ProfiledModalInstance['state'] & InstructionMountProps<ReturnType<ProfiledModalInstance['getInfo']>>> = (props) => {
  const { closeResolve } = props;
  const forceUpdate = useUpdate();
  const pageInstance = useCreation(() => new ProfiledModalInstance({ forceUpdate, state: { ...props } }), []);

  const { imgConfig, maskConfig } = pageInstance.state;

  return (
    <pageInstance.Modal
      afterClose={() => {
        closeResolve(pageInstance.isClickConfirm ? pageInstance.getInfo() : null)
      }}
    >
      {pageInstance.loading && <Spin />}
      {!pageInstance.loading && (
        <div>
          <div
            style={{
              position: 'relative',
              width: imgConfig.width,
              height: imgConfig.height,
            }}
          >
            <div
              style={{
                position: 'absolute',
                width: '100%',
                height: '100%',
              }}
            >
              <img
                src={imgConfig.src}
                alt=""
                className={styles.editImg}
              />
              <div className={styles.editImgMask} />
            </div>
            <img
              src={imgConfig.src}
              alt=""
              className={styles.editImg}
              style={{
                WebkitMaskImage: `url(${maskConfig.src})`,
                WebkitMaskRepeat: 'no-repeat',
                WebkitMaskSize: `${maskConfig.width}px ${maskConfig.height}px`,
                WebkitMaskPosition: `${maskConfig.offsetX}px ${maskConfig.offsetY}px`,
              }}
            />
          </div>
          <div>
            <div className={cx(EmotionBaseInstance.flexRowCenter)}>
              <div>scale</div>
              <Slider
                className={css`width: 100%;margin-left: 10px;`}
                value={maskConfig.width}
                max={imgConfig.width}
                min={10}
                onChange={(e) => {
                  maskConfig.width = e;
                  maskConfig.height = (maskConfig.width / maskConfig.naturalWidth) * maskConfig.naturalHeight;
                  pageInstance.forceUpdate();
                }}
              />
            </div>
            <div className={cx(EmotionBaseInstance.flexRowCenter)}>
              <div>offsetX</div>
              <Slider
                className={css`width: 100%;margin-left: 10px;`}
                value={maskConfig.offsetX}
                onChange={(e) => {
                  maskConfig.offsetX = e;
                  pageInstance.forceUpdate();
                }}
              />
            </div>
            <div className={cx(EmotionBaseInstance.flexRowCenter)}>
              <div>offsetY</div>
              <Slider
                className={css`width: 100%;margin-left: 10px;`}
                value={maskConfig.offsetY}
                onChange={(e) => {
                  maskConfig.offsetY = e;
                  pageInstance.forceUpdate();
                }}
              />
            </div>
          </div>
        </div>
      )}
    </pageInstance.Modal>
  );
};

export const openProfiledModalPromise = (args: ProfiledModalInstance['state']) => {
  return appInstance.getContextHolder().instructionMountPromise<ProfiledModalInstance['state'], ReturnType<ProfiledModalInstance['getInfo']>>({
    Component: ProfiledModal,
    props: args,
  });
};

使用如下:

ts 复制代码
onClick={() => {
  openProfiledModalPromise({
    imgConfig: { src: 'http://127.0.0.1:9000/test/1111.png' },
    maskConfig: { src: 'http://127.0.0.1:9000/test/2222.png' },
  });
}}

组件细节需要后面完善,这里只给出大体

组件指令式形式参考文章,Modal弹窗是antd的Modal简单封装

将过程转移到组件中倒是不必使用前文说的不断修改参数进行交互了,如果有大佬有好的方法使用leaferjs本身进行交互的话也可以评论区告知一下

相关推荐
Dolphin_海豚21 分钟前
vapor 语法糖是如何被解析的
前端·源码·vapor
Bdygsl1 小时前
前端开发:HTML(5)—— 表单
前端·html
望获linux1 小时前
【实时Linux实战系列】实时数据流处理框架分析
linux·运维·前端·数据库·chrome·操作系统·wpf
国家不保护废物2 小时前
TailwindCSS:原子化CSS的革命,让React开发爽到飞起!🚀
前端·css·react.js
程序视点2 小时前
如何高效率使用 Cursor ?
前端·后端·cursor
前端领航者2 小时前
重学Vue3《 v-for的key属性:性能差异与最佳实践》
前端·javascript
归于尽2 小时前
跨域问题从青铜到王者:JSONP、CORS原理详解与实战(前端必会)
前端·浏览器
Andy_GF2 小时前
纯血鸿蒙HarmonyOS Next 远程测试包分发
前端·ios·harmonyos
嗑药狂写9W行代码3 小时前
cesium修改源码支持4490坐标系
前端