Skip to content

Commit

Permalink
feat: 添加基础图片裁切功能 (#405)
Browse files Browse the repository at this point in the history
* feat: 添加编辑多边形plugin功能

* refactor: 使用mixinState.mSelectOneType判定编辑多边形按钮的显示与否

* feat: 添加动态创建多边形插件

* feat: 添加绘制多边形DrawPolygonPlugin

* refactor: 抽离shiftAngle方法用于shiftKey的时候纠正角度

* refactor: 修改绘制polygon的交互逻辑

* feat: 添加shiftKey纠正点位和onEnd监听,并完善交互逻辑

* feat: 添加自由绘制功能

* refactor: 完善历史记录和绘制多边形结合交互

* refactor: 完善判定开启和关闭特殊tool时的副作用

* feat: 添加pathTextPlugin用户绘制路径文字

* feat: 添加裁切图片的功能

* refactor: 完善裁切shell的附带信息

* refactor: 选中裁切object时,逻辑状态改为取消选中状态

---------

Co-authored-by: weicheng.liang <[email protected]>
  • Loading branch information
ByeWord and weicheng.liang authored Jun 2, 2024
1 parent 76533e4 commit 147763c
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export { default as DrawPolygonPlugin } from './plugin/DrawPolygonPlugin';
export { default as FreeDrawPlugin } from './plugin/FreeDrawPlugin';
export { default as PathTextPlugin } from './plugin/PathTextPlugin';
export { default as PsdPlugin } from './plugin/PsdPlugin';
export { default as SimpleClipImagePlugin } from './plugin/SimpleClipImagePlugin';
import EventType from './eventType';
import Utils from './utils/utils';

Expand Down
5 changes: 3 additions & 2 deletions packages/core/plugin/PolygonModifyPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ function renderIconEdge(
left: number,
top: number,
styleOverride: any,
fabricObject: fabric.Object,
img: HTMLImageElement
fabricObject: fabric.Object
) {
const img = document.createElement('img');
img.src = edgeImg;
drawImg(ctx, left, top, img, 25, 25, fabric.util.degreesToRadians(fabricObject.angle || 0));
}

Expand Down
194 changes: 194 additions & 0 deletions packages/core/plugin/SimpleClipImagePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { fabric } from 'fabric';
import Editor from '../Editor';
import { getPolygonVertices } from '../../../src/utils/math';
import { get, set } from 'lodash-es';
type IEditor = Editor;

const getBounds = (activeObject: fabric.Object) => {
const { left = 0, top = 0 } = activeObject;
return {
width: activeObject.getScaledWidth(),
height: activeObject.getScaledHeight(),
left,
top,
};
};
const bindInfo = (shell: fabric.Object, activeObject: fabric.Object) => {
bindFlagToObject(shell);
bindFlagToObject(shell, 'targetId', get(activeObject, 'id'));
bindFlagToObject(shell, 'targetType', get(activeObject, 'type'));
};
const bindFlagToObject = (activeObject: fabric.Object, key = 'clip', value: any = true) => {
set(activeObject, key, value);
};
const createRectClip = (activeObject: fabric.Object, inverted: boolean) => {
const { width = 0, height = 0, left = 0, top = 0 } = getBounds(activeObject);
const clipW = Math.round(width / 2);
const clipH = Math.round(height / 2);
const shell = new fabric.Rect({
width: clipW,
height: clipH,
fill: 'rgba(0,0,0,0)',
originX: 'center',
originY: 'center',
left: left + width / 2,
top: top + height / 2,
});
bindInfo(shell, activeObject);
const clipPath = new fabric.Rect({
absolutePositioned: true,
width: shell.width,
height: shell.height,
originX: 'center',
originY: 'center',
left: shell.left,
top: shell.top,
inverted: inverted,
});
return { clipPath, shell };
};
const createCircleClip = (activeObject: fabric.Object, inverted: boolean) => {
const point = activeObject.getCenterPoint();
const { width } = getBounds(activeObject);
const shell = new fabric.Circle({
fill: 'rgba(0,0,0,0)',
originX: 'center',
originY: 'center',
left: point.x,
top: point.y,
radius: width / 2,
});
bindInfo(shell, activeObject);
const clipPath = new fabric.Circle({
absolutePositioned: true,
originX: 'center',
originY: 'center',
left: shell.left,
top: shell.top,
radius: width / 2,
inverted: inverted,
});
return { shell, clipPath };
};
const createTriClip = (activeObject: fabric.Object, inverted: boolean) => {
const point = activeObject.getCenterPoint();
const { width = 0, height = 0 } = getBounds(activeObject);
const clipW = Math.round(width / 2);
const clipH = Math.round(height / 2);
const shell = new fabric.Triangle({
fill: 'rgba(0,0,0,0)',
originX: 'center',
originY: 'center',
left: point.x,
top: point.y,
width: clipW,
height: clipH,
});
bindInfo(shell, activeObject);
const clipPath = new fabric.Triangle({
absolutePositioned: true,
originX: 'center',
originY: 'center',
left: shell.left,
top: shell.top,
width: shell.width,
height: shell.height,
inverted: inverted,
});
return { shell, clipPath };
};
const createPolygonClip = (activeObject: fabric.Object, inverted: boolean) => {
const point = activeObject.getCenterPoint();
const points = getPolygonVertices(5, 200);
const shell = new fabric.Polygon(points, {
fill: 'rgba(0,0,0,0)',
originY: 'center',
originX: 'center',
left: point.x,
top: point.y,
});
bindInfo(shell, activeObject);
const clipPath = new fabric.Polygon([...points], {
absolutePositioned: true,
originX: 'center',
originY: 'center',
left: shell.left,
top: shell.top,
inverted: inverted,
});
return { shell, clipPath };
};
export default class SimpleClipImagePlugin {
static pluginName = 'SimpleClipImagePlugin';
// static events = ['sizeChange'];
static apis = ['addClipPathToImage', 'removeClip'];
constructor(public canvas: fabric.Canvas, public editor: IEditor) {}
addClipPathToImage(value: string) {
const activeObject = this.canvas.getActiveObjects()[0];
if (activeObject && activeObject.type === 'image') {
let clip: { shell: fabric.Object; clipPath: fabric.Object } | null = null;
const [name, inverted] = value.split('-');
const isInverted = !!inverted;
switch (name) {
case 'polygon':
clip = createPolygonClip(activeObject, isInverted);
break;
case 'rect':
clip = createRectClip(activeObject, isInverted);
break;
case 'circle':
clip = createCircleClip(activeObject, isInverted);
break;
case 'triangle':
clip = createTriClip(activeObject, isInverted);
break;
}
if (clip == null) return;
const { shell, clipPath } = clip;
shell.on('moving', () => {
clipPath.setPositionByOrigin(shell.getCenterPoint(), 'center', 'center');
activeObject.set('dirty', true);
});
shell.on('rotating', () => {
clipPath.set({ angle: shell.angle });
activeObject.set('dirty', true);
});
shell.on('scaling', () => {
clipPath.set({ scaleX: shell.scaleX, scaleY: shell.scaleY });
clipPath.setPositionByOrigin(shell.getCenterPoint(), 'center', 'center');
activeObject.set('dirty', true);
});
shell.on('deselected', () => {
const position = activeObject.toLocalPoint(shell.getCenterPoint(), 'center', 'center');
clipPath.set({
absolutePositioned: false,
left: position.x,
top: position.y,
});
if (clipPath instanceof fabric.Circle) {
clipPath.set({ radius: (shell as fabric.Circle).getRadiusX(), scaleY: 1, scaleX: 1 });
} else {
clipPath.set({
width: shell.getScaledWidth(),
height: shell.getScaledHeight(),
scaleX: 1,
scaleY: 1,
});
}
this.canvas.remove(shell);
this.canvas.requestRenderAll();
});
activeObject.set({ clipPath: clipPath });
this.canvas.add(shell);
this.canvas.setActiveObject(shell);
}
}
removeClip() {
const activeObject = this.canvas.getActiveObjects()[0];
if (activeObject && activeObject.type === 'image') {
activeObject.set({ clipPath: undefined });
activeObject.set('dirty', true);
this.canvas.requestRenderAll();
}
}
}
90 changes: 90 additions & 0 deletions src/components/clipImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<template>
<div v-if="mixinState.mSelectMode === 'one' && type === 'image'" class="attr-item-box">
<div class="bg-item ivu-mb-8">
<Dropdown style="width: 270px" @on-click="addClipPath">
<Button type="text" long>{{ $t('createClip') }}</Button>
<template #list>
<DropdownMenu>
<DropdownItem v-for="item in options" :key="item.value" :name="item.value">
{{ item.label }}
</DropdownItem>
</DropdownMenu>
</template>
</Dropdown>
</div>
<div class="bg-item">
<Button @click="removeClip" type="text" long>{{ $t('removeClip') }}</Button>
</div>
</div>
</template>

<script setup name="ReplaceImg">
import useSelect from '@/hooks/select';
import { useI18n } from 'vue-i18n';
const update = getCurrentInstance();
// const canvasEditor = inject('canvasEditor');
const { mixinState, canvasEditor } = useSelect();
const { t } = useI18n();
const type = ref('');
const options = [
{
label: t('polygonClip'),
value: 'polygon',
},
{
label: t('rectClip'),
value: 'rect',
},
{
label: t('circleClip'),
value: 'circle',
},
{
label: t('triangleClip'),
value: 'triangle',
},
{
label: t('polygonClipInverted'),
value: 'polygon-inverted',
},
{
label: t('rectClipInverted'),
value: 'rect-inverted',
},
{
label: t('circleClipInverted'),
value: 'circle-inverted',
},
{
label: t('triangleClipInverted'),
value: 'triangle-inverted',
},
];
const addClipPath = async (name) => {
canvasEditor.addClipPathToImage(name);
};
const removeClip = () => {
canvasEditor.removeClip();
};
const init = () => {
const activeObject = canvasEditor.canvas.getActiveObjects()[0];
if (activeObject) {
type.value = activeObject.type;
update?.proxy?.$forceUpdate();
}
};
onMounted(() => {
canvasEditor.on('selectOne', init);
});
onBeforeUnmount(() => {
canvasEditor.off('selectOne', init);
});
</script>
<style lang="less" scoped>
.attr-item-box {
margin-top: 8px;
}
</style>
2 changes: 0 additions & 2 deletions src/components/importJSON.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ import useMaterial from '@/hooks/useMaterial';
import { Message } from 'view-ui-plus';
import modalSzie from '@/components/common/modalSzie';
import { Spin } from 'view-ui-plus';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { canvasEditor } = useSelect();
const { createTmpl, routerToId } = useMaterial();
Expand Down
8 changes: 8 additions & 0 deletions src/hooks/useSelectListen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
import Editor, { EventType } from '@kuaitu/core';
const { SelectEvent, SelectMode } = EventType;
import { get } from 'lodash-es';
interface Selector {
mSelectMode: SelectMode;
mSelectOneType: string | undefined;
Expand All @@ -27,6 +28,13 @@ export default function useSelectListen(canvasEditor: Editor) {

const selectOne = (e: [fabric.Object]) => {
state.mSelectMode = SelectMode.ONE;
if (e[0] && get(e[0], 'clip')) {
selectCancel();
// state.mSelectId = get(e[0], 'targetId');
// state.mSelectOneType = get(e[0], 'targetType');
// state.mSelectIds = e.map((item) => get(item, 'targetId'));
return;
}
if (e[0]) {
state.mSelectId = e[0].id;
state.mSelectOneType = e[0].type;
Expand Down
12 changes: 11 additions & 1 deletion src/language/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@
},
"select_json": "选择JSON文件",
"repleaceImg": "替换图片",
"createClip": "添加裁切",
"removeClip": "移除裁切",
"polygonClip": "多边形",
"rectClip": "矩形",
"circleClip": "",
"triangleClip": "三角形",
"polygonClipInverted": "多边形【反】",
"rectClipInverted": "矩形【反】",
"circleClipInverted": "圆【反】",
"triangleClipInverted": "三角形【反】",
"waterMark": {
"text": "水印",
"modalTitle": "配置水印",
Expand Down Expand Up @@ -229,4 +239,4 @@
"myMaterial": {
"uploadBtn": "上传素材"
}
}
}
5 changes: 5 additions & 0 deletions src/views/home/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@
<center-align></center-align>
<!-- 替换图片 -->
<replaceImg></replaceImg>
<!-- 图片裁切 -->
<clip-image></clip-image>
<!-- 翻转 -->
<flip></flip>
<!-- 图片滤镜 -->
Expand Down Expand Up @@ -242,8 +244,10 @@ import Editor, {
FreeDrawPlugin,
PathTextPlugin,
PsdPlugin,
SimpleClipImagePlugin,
} from '@kuaitu/core';
import Edit from '@/components/edit.vue';
import ClipImage from '@/components/clipImage.vue';
import AttributeTextContent from '@/components/attributeTextContent.vue';
// 创建编辑器
Expand Down Expand Up @@ -340,6 +344,7 @@ onMounted(() => {
canvasEditor.use(DrawPolygonPlugin);
canvasEditor.use(FreeDrawPlugin);
canvasEditor.use(PathTextPlugin);
canvasEditor.use(SimpleClipImagePlugin);
canvasEditor.use(FontPlugin, {
repoSrc: APIHOST,
});
Expand Down

0 comments on commit 147763c

Please sign in to comment.