feat: 旋转 有bug 旋转后不能正确缩放

Re-1.0
咬轮猫 3 years ago
parent afd91e80e3
commit 473b947c73

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44C30.9566 44 37.0836 40.4483 40.6667 35.0593" stroke="#333" stroke-width="4" stroke-linecap="round"/><path d="M44 24H30" stroke="#333" stroke-width="4" stroke-linecap="round"/><circle cx="24" cy="24" r="6" fill="#333" stroke="#333" stroke-width="4"/></svg>

After

Width:  |  Height:  |  Size: 481 B

@ -27,64 +27,75 @@
:key="item.id"
:transform="`translate(${item.x},${item.y})rotate(0)scale(1)`"
>
<use
:xlink:href="`#svg-${item.name}`"
fill="#ff0000"
width="100"
height="100"
:id="item.id"
<g
:transform="`translate(${item.actual_bound.x + item.actual_bound.width / 2},${
item.actual_bound.y + item.actual_bound.height / 2
}) scale(${item.scale_x},${item.scale_y}) translate(${-(
})rotate(${item.rotate}) scale(1) translate(${-(
item.actual_bound.x +
item.actual_bound.width / 2
)},${-(item.actual_bound.y + item.actual_bound.height / 2)})`"
></use>
<rect
:id="`rect${item.id}`"
fill="black"
fill-opacity="0"
:x="
item.actual_bound.x -
(item.actual_bound.width / 2) * item.scale_x +
item.actual_bound.width / 2
"
:y="
item.actual_bound.y -
(item.actual_bound.height / 2) * item.scale_y +
item.actual_bound.height / 2
"
:width="item.actual_bound.width * item.scale_x"
:height="item.actual_bound.height * item.scale_y"
style="stroke: none; stroke-width: 2; stroke-miterlimit: 10"
:class="`${globalStore.intention == EGlobalStoreIntention.None ? 'svg-item-none' : ''}
>
<use
:xlink:href="`#svg-${item.name}`"
fill="#ff0000"
width="100"
height="100"
:id="item.id"
:transform="`translate(${item.actual_bound.x + item.actual_bound.width / 2},${
item.actual_bound.y + item.actual_bound.height / 2
}) scale(${item.scale_x},${item.scale_y}) translate(${-(
item.actual_bound.x +
item.actual_bound.width / 2
)},${-(item.actual_bound.y + item.actual_bound.height / 2)})`"
></use>
<rect
:id="`rect${item.id}`"
fill="black"
fill-opacity="0"
:x="
item.actual_bound.x -
(item.actual_bound.width / 2) * item.scale_x +
item.actual_bound.width / 2
"
:y="
item.actual_bound.y -
(item.actual_bound.height / 2) * item.scale_y +
item.actual_bound.height / 2
"
:width="item.actual_bound.width * item.scale_x"
:height="item.actual_bound.height * item.scale_y"
style="stroke: none; stroke-width: 2; stroke-miterlimit: 10"
:class="`${globalStore.intention == EGlobalStoreIntention.None ? 'svg-item-none' : ''}
${
globalStore.intention == EGlobalStoreIntention.Move &&
globalStore.handle_svg_info?.info.id == item.id
? 'svg-item-move'
: ''
} ${
globalStore.intention == EGlobalStoreIntention.Select &&
globalStore.handle_svg_info?.info.id == item.id
? 'svg-item-select'
: ''
}`"
@mousedown="onSvgMouseDown(item, index, $event)"
></rect>
<handle-panel
v-if="
globalStore.handle_svg_info?.info.id == item.id &&
(globalStore.intention == EGlobalStoreIntention.Select ||
globalStore.intention == EGlobalStoreIntention.Zoom)
"
:item-info="item"
></handle-panel>
globalStore.intention == EGlobalStoreIntention.Select &&
globalStore.handle_svg_info?.info.id == item.id
? 'svg-item-select'
: ''
}`"
@mousedown="onSvgMouseDown(item, index, $event)"
></rect>
<handle-panel
v-if="
globalStore.handle_svg_info?.info.id == item.id &&
(globalStore.intention === EGlobalStoreIntention.Select ||
globalStore.intention === EGlobalStoreIntention.Zoom ||
globalStore.intention === EGlobalStoreIntention.Rotate)
"
:item-info="item"
></handle-panel>
</g>
</g>
</g>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useConfigStore } from '../../../../store/config';
import { useGlobalStore } from '../../../../store/global';
import {
@ -100,6 +111,13 @@
const globalStore = useGlobalStore();
const configStore = useConfigStore();
const svgEditLayoutStore = useSvgEditLayoutStore();
const cursor_style = computed(() =>
globalStore.intention == EGlobalStoreIntention.MoveCanvas
? 'grab'
: globalStore.intention == EGlobalStoreIntention.Rotate
? "url('/src/assets/icons/rotate.svg') 12 12, auto"
: 'default'
);
const dropEvent = (e: DragEvent) => {
if (globalStore.intention == EGlobalStoreIntention.None) {
return;
@ -112,8 +130,13 @@
id: globalStore.create_svg_info.name + randomString(),
x: e.offsetX - svgEditLayoutStore.center_offset.x,
y: e.offsetY - svgEditLayoutStore.center_offset.y,
client: {
x: e.clientX,
y: e.clientY
},
scale_x: 1,
scale_y: 1,
rotate: 0,
actual_bound: {
x: 0,
y: 0,
@ -174,7 +197,7 @@
//
svgEditLayoutStore.center_offset.x = globalStore.mouse_info.new_position_x;
svgEditLayoutStore.center_offset.y = globalStore.mouse_info.new_position_y;
} else if (globalStore.intention == EGlobalStoreIntention.Zoom) {
} else if (globalStore.intention === EGlobalStoreIntention.Zoom) {
//
const move_length_x =
globalStore.scale_info.type === EScaleInfoType.TopLeft ||
@ -227,6 +250,24 @@
: globalStore.scale_info.scale_item_info.y - move_length_y / 2;
}
}
} else if (globalStore.intention === EGlobalStoreIntention.Rotate) {
if (!globalStore.handle_svg_info) {
return;
}
const rotateDegreeBefore =
Math.atan2(
globalStore.mouse_info.position_y - globalStore.handle_svg_info.info.client.y,
globalStore.mouse_info.position_x - globalStore.handle_svg_info.info.client.x
) /
(Math.PI / 180);
const rotateDegreeAfter =
Math.atan2(
clientY - globalStore.handle_svg_info.info.client.y,
clientX - globalStore.handle_svg_info.info.client.x
) /
(Math.PI / 180);
globalStore.handle_svg_info.info.rotate =
globalStore.rotate_info.angle + rotateDegreeAfter - rotateDegreeBefore;
}
};
const onCanvasMouseUp = () => {
@ -274,7 +315,7 @@
.canvas {
width: 100%;
height: 100%;
cursor: v-bind('globalStore.intention == EGlobalStoreIntention.MoveCanvas?"grab":"default"');
cursor: v-bind('cursor_style');
}
.svg-item-none {

@ -7,19 +7,31 @@
width="8"
height="8"
:fill="fill"
style="cursor: nw-resize; vector-effect: non-scaling-stroke"
:style="{cursor: getCursor(0).cursor, 'vector-effect': 'non-scaling-stroke'}"
pointer-events="all"
:x="props.itemInfo.actual_bound.x-offset-getCoordinateOffset(props.itemInfo.actual_bound.width,props.itemInfo.scale_x)"
:y="props.itemInfo.actual_bound.y-offset-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
stroke="rgba(0,0,0,0)"
@mousedown="onHandleMouseDown(EScaleInfoType.TopLeft,$event)"
></rect
><rect
@mousedown="onHandleMouseDown(EScaleInfoType.TopLeft,$event)">
</rect>
<circle
:cx="props.itemInfo.actual_bound.x+props.itemInfo.actual_bound.width/2"
:cy="props.itemInfo.actual_bound.y-offset-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)-24"
:r="4"
class="rotate-circle"
@mousedown="onRotateCircleMouseDown"/>
<line
:x1="props.itemInfo.actual_bound.x+props.itemInfo.actual_bound.width/2"
:y1="props.itemInfo.actual_bound.y-offset-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
:x2="props.itemInfo.actual_bound.x+props.itemInfo.actual_bound.width/2"
:y2="props.itemInfo.actual_bound.y-offset-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)-20"
:style="{stroke:fill,'stroke-width':2}" />
<rect
id="resize_tc"
width="8"
height="8"
:fill="fill"
style="cursor: n-resize; vector-effect: non-scaling-stroke"
:style="{cursor: getCursor(45).cursor, 'vector-effect': 'non-scaling-stroke'}"
pointer-events="all"
:x="props.itemInfo.actual_bound.x+props.itemInfo.actual_bound.width/2-offset"
:y="props.itemInfo.actual_bound.y-offset-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
@ -31,7 +43,7 @@
width="8"
height="8"
:fill="fill"
style="cursor: ne-resize; vector-effect: non-scaling-stroke"
:style="{cursor: getCursor(90).cursor, 'vector-effect': 'non-scaling-stroke'}"
pointer-events="all"
:x="props.itemInfo.actual_bound.x+props.itemInfo.actual_bound.width-offset+getCoordinateOffset(props.itemInfo.actual_bound.width,props.itemInfo.scale_x)"
:y="props.itemInfo.actual_bound.y-offset-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
@ -43,7 +55,7 @@
width="8"
height="8"
:fill="fill"
style="cursor: e-resize; vector-effect: non-scaling-stroke"
:style="{cursor: getCursor(315).cursor, 'vector-effect': 'non-scaling-stroke'}"
pointer-events="all"
:x="props.itemInfo.actual_bound.x-offset-getCoordinateOffset(props.itemInfo.actual_bound.width,props.itemInfo.scale_x)"
:y="props.itemInfo.actual_bound.y-offset+props.itemInfo.actual_bound.height*props.itemInfo.scale_y/2-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
@ -55,7 +67,7 @@
width="8"
height="8"
:fill="fill"
style="cursor: w-resize; vector-effect: non-scaling-stroke"
:style="{cursor: getCursor(135).cursor, 'vector-effect': 'non-scaling-stroke'}"
pointer-events="all"
:x="props.itemInfo.actual_bound.x-offset+props.itemInfo.actual_bound.width+getCoordinateOffset(props.itemInfo.actual_bound.width,props.itemInfo.scale_x)"
:y="props.itemInfo.actual_bound.y-offset+props.itemInfo.actual_bound.height*props.itemInfo.scale_y/2-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
@ -67,7 +79,7 @@
width="8"
height="8"
:fill="fill"
style="cursor: sw-resize; vector-effect: non-scaling-stroke"
:style="{cursor: getCursor(270).cursor, 'vector-effect': 'non-scaling-stroke'}"
pointer-events="all"
:x="props.itemInfo.actual_bound.x-offset-getCoordinateOffset(props.itemInfo.actual_bound.width,props.itemInfo.scale_x)"
:y="props.itemInfo.actual_bound.y-offset+props.itemInfo.actual_bound.height*props.itemInfo.scale_y-getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
@ -79,7 +91,7 @@
width="8"
height="8"
:fill="fill"
style="cursor: s-resize; vector-effect: non-scaling-stroke"
:style="{cursor: getCursor(225).cursor, 'vector-effect': 'non-scaling-stroke'}"
pointer-events="all"
:x="props.itemInfo.actual_bound.x-offset+props.itemInfo.actual_bound.width/2"
:y="props.itemInfo.actual_bound.y-offset+props.itemInfo.actual_bound.height+getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
@ -91,7 +103,7 @@
width="8"
height="8"
:fill="fill"
style="cursor: se-resize; vector-effect: non-scaling-stroke"
:style="{cursor: getCursor(180).cursor, 'vector-effect': 'non-scaling-stroke'}"
pointer-events="all"
:x="props.itemInfo.actual_bound.x-offset+props.itemInfo.actual_bound.width+getCoordinateOffset(props.itemInfo.actual_bound.width,props.itemInfo.scale_x)"
:y="props.itemInfo.actual_bound.y-offset+props.itemInfo.actual_bound.height+getCoordinateOffset(props.itemInfo.actual_bound.height,props.itemInfo.scale_y)"
@ -117,8 +129,18 @@
}
});
const globalStore = useGlobalStore();
const offset = ref(5);
const offset = ref(4);
const fill = ref('#4F80FF');
const angle_to_cursor = [
{ start: 338, end: 23, cursor: 'nw', type: EScaleInfoType.TopLeft },
{ start: 23, end: 68, cursor: 'n', type: EScaleInfoType.TopCenter },
{ start: 68, end: 113, cursor: 'ne', type: EScaleInfoType.TopRight },
{ start: 293, end: 338, cursor: 'w', type: EScaleInfoType.Left },
{ start: 113, end: 158, cursor: 'e', type: EScaleInfoType.Right },
{ start: 248, end: 293, cursor: 'sw', type: EScaleInfoType.BottomLeft },
{ start: 203, end: 248, cursor: 's', type: EScaleInfoType.BottomCenter },
{ start: 158, end: 203, cursor: 'se', type: EScaleInfoType.BottomRight }
];
const onHandleMouseDown = (type: EScaleInfoType, e: MouseEvent) => {
const { clientX, clientY } = e;
e.cancelBubble = true;
@ -144,5 +166,49 @@
}
});
};
const onRotateCircleMouseDown = (e: MouseEvent) => {
const { clientX, clientY } = e;
e.cancelBubble = true;
globalStore.intention = EGlobalStoreIntention.Rotate;
globalStore.rotate_info = {
angle: props.itemInfo.rotate
};
globalStore.setMouseInfo({
state: EMouseInfoState.Down,
position_x: clientX,
position_y: clientY,
now_position_x: clientX,
now_position_y: clientY,
new_position_x: 0,
new_position_y: 0
});
};
/**
* 获取旋转之后的光标样式
* @param init_angle 初始角度 360/8=45
*/
const getCursor = (init_angle: number) => {
const now_init_angle = (init_angle + props.itemInfo.rotate) % 360;
const find_cursor = angle_to_cursor.find(
(f) => f.start <= now_init_angle && f.end > now_init_angle
);
if (!find_cursor) {
return {
cursor: 'nw-resize',
type: EScaleInfoType.TopLeft
};
}
return {
cursor: find_cursor.cursor + '-resize',
type: find_cursor.type
};
};
</script>
<style scoped></style>
<style scoped>
.rotate-circle {
stroke: v-bind('fill');
stroke-width: 1;
fill-opacity: 0;
cursor: url('@/assets/icons/rotate.svg') 12 12, auto;
}
</style>

@ -42,6 +42,9 @@ export const useGlobalStore = defineStore('global-store', {
x: 0,
y: 0
}
},
rotate_info: {
angle: 0
}
};
},

@ -8,13 +8,19 @@ export interface IGlobalStore {
mouse_info: IMouseInfo;
handle_svg_info: IHandleSvgInfo | undefined;
scale_info: IScaleInfo;
rotate_info: IRotateInfo;
}
export interface IDoneJson extends IConfigItem {
id: string;
x: number;
y: number;
client: {
x: number;
y: number;
};
scale_x: number;
scale_y: number;
rotate: number;
actual_bound: {
x: number;
y: number;
@ -28,7 +34,8 @@ export enum EGlobalStoreIntention {
Move = 'Move',
MoveCanvas = 'MoveCanvas',
Select = 'Select',
Zoom = 'Zoom'
Zoom = 'Zoom',
Rotate = 'Rotate'
}
export interface IMouseInfo {
state: EMouseInfoState;
@ -62,6 +69,12 @@ export interface IScaleInfo {
y: number;
};
}
/**
*
*/
export interface IRotateInfo {
angle: number;
}
export enum EScaleInfoType {
None = '',
TopLeft = 'TopLeft',

@ -24,5 +24,10 @@ export default defineConfig({
// 禁用压缩 否则想要修改无状态组件的stroke或者fill会影响到预设样式 例如stroke-width
svgoOptions: false
})
]
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
});

Loading…
Cancel
Save