图像编辑器一大功能便是裁切,介于之前公司里面做过一个简单的编辑器,直接套用那个编辑器的裁切功能,而那个裁切重要的是要能够异形(各种稀奇古怪的形状)
查询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本身进行交互的话也可以评论区告知一下