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本身进行交互的话也可以评论区告知一下

相关推荐
PineappleCoder2 小时前
搞定用户登录体验:双 Token 认证(Vue+Koa2)从 0 到 1 实现无感刷新
前端·vue.js·koa
EveryPossible2 小时前
展示内容框
前端·javascript·css
伊织code3 小时前
WebGoat - 刻意设计的不安全Web应用程序
前端·安全·webgoat
子兮曰3 小时前
Vue3 生命周期与组件通信深度解析
前端·javascript·vue.js
拉不动的猪3 小时前
回顾关于筛选时的隐式返回和显示返回
前端·javascript·面试
yinuo3 小时前
不写一行JS!纯CSS如何读取HTML属性实现Tooltip
前端
gnip3 小时前
脚本加载失败重试机制
前端·javascript
遗憾随她而去.3 小时前
Uni-App 页面跳转监控实战:快速定位路由问题
前端·网络·uni-app
码农学院4 小时前
MSSQL字段去掉excel复制过来的换行符
前端·数据库·sqlserver
颜酱5 小时前
实现一个mini编译器,来感受编译器的各个流程
前端·javascript·编译器