我一直在尝试制作一个 svg 编辑器,但在调整线条元素的大小时遇到了麻烦。该行不会留在选择框内并与选择框一起调整大小。在下面的 gif 中,您可以看到 line 的问题:
我已经使用这个项目在 svelte 中实现了选择框部分,但到目前为止,对于需要调整大小的实际线条元素,我无法做到这一点。我可以使用控件坐标,但这对于路径之类的元素并不理想,或者我不知道如何做到这一点。我只是无法弄清楚用于选择框的数学背后的数学原理。我已经这样做了一段时间,所以至少有一些指针会受到赞赏。您可以在此代码框中签出我的项目,它在打字稿中。
该文件具有用于 svelte 选择框组件的辅助函数。getCalcedPosSize
函数是选择框矩形元素属性获取新值的地方。
import type { Point } from '../types/svg';
export const getLength = (x, y) => Math.sqrt(x * x + y * y);
export const getAngle = ({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point) => {
const dot = x1 * x2 + y1 * y2
const det = x1 * y2 - y1 * x2
const angle = Math.atan2(det, dot) / Math.PI * 180
return (angle + 360) % 360
}
export const degToRadian = (deg) => deg * Math.PI / 180
export const cos = (deg) => Math.cos(degToRadian(deg))
export const sin = (deg) => Math.sin(degToRadian(deg))
const setWidthAndDeltaW = (width, deltaW, minWidth) => {
const expectedWidth = width + deltaW
if (expectedWidth > minWidth) {
width = expectedWidth
} else {
deltaW = minWidth - width
width = minWidth
}
return { width, deltaW }
}
const setHeightAndDeltaH = (height, deltaH, minHeight) => {
const expectedHeight = height + deltaH
if (expectedHeight > minHeight) {
height = expectedHeight
} else {
deltaH = minHeight - height
height = minHeight
}
return { height, deltaH }
}
export const getCalcedPosSize = (type, rect, deltaW, deltaH, ratio, minWidth, minHeight) => {
ratio = undefined
let { width, height, centerX, centerY, rotateAngle } = rect
const widthFlag = width < 0 ? -1 : 1
const heightFlag = height < 0 ? -1 : 1
width = Math.abs(width)
height = Math.abs(height)
switch (type) {
case 'e': {
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
if (ratio) {
deltaH = deltaW / ratio
height = width / ratio
centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle)
} else {
centerX += deltaW / 2 * cos(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle)
}
break
}
case 'ne': {
deltaH = -deltaH
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
deltaW = deltaH * ratio
width = height * ratio
}
centerX += deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle)
break
}
case 'se': {
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
deltaW = deltaH * ratio
width = height * ratio
}
centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle)
break
}
case 's': {
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
deltaW = deltaH * ratio
width = height * ratio
centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle)
} else {
centerX -= deltaH / 2 * sin(rotateAngle)
centerY += deltaH / 2 * cos(rotateAngle)
}
break
}
case 'sw': {
deltaW = -deltaW
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
height = width / ratio
deltaH = deltaW / ratio
}
centerX -= deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle)
centerY -= deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle)
break
}
case 'w': {
deltaW = -deltaW
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
if (ratio) {
height = width / ratio
deltaH = deltaW / ratio
centerX -= deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle)
centerY -= deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle)
} else {
centerX -= deltaW / 2 * cos(rotateAngle)
centerY -= deltaW / 2 * sin(rotateAngle)
}
break
}
case 'nw': {
deltaW = -deltaW
deltaH = -deltaH
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
width = height * ratio
deltaW = deltaH * ratio
}
centerX -= deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle)
centerY -= deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle)
break
}
case 'n': {
deltaH = -deltaH
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
width = height * ratio
deltaW = deltaH * ratio
centerX += deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle)
} else {
centerX += deltaH / 2 * sin(rotateAngle)
centerY -= deltaH / 2 * cos(rotateAngle)
}
break
}
}
return {
position: {
centerX,
centerY
},
size: {
width: width * widthFlag,
height: height * heightFlag
}
}
}
export function getDistance(p1: Point, p2: Point) {
let dist = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2))
return dist;
}
export function getNearestPoint(arr: Array<Point>, point: Point) {
let min = Infinity;
let result = arr[0];
let i = 0;
let i_arr = 0;
arr.forEach(a => {
let dist = getDistance(a, point);
if (dist > min) {
min = dist
result = a;
i_arr = i;
}
i++;
})
return {
i: i_arr,
point: result
};
}
这是选择框纤细的组件。
<script lang="ts">
import { getLength, getAngle, getDistance } from './utils/utils'
import { getCalcedPosSize, degToRadian, sin, cos } from './utils/utils'
import type { Line, Rect, Path } from './types/svg'
export let rotatable=true, width=100, height=100, top=150, left=150, centerX, centerY, rotateAngle=0, minWidth=0.5, minHeight=0.5, elements:Array<Line|Rect|Path>|Line|Rect|Path;
let color="#008EFF", ctrlWidth=10, strokeWidth=2, selectBox;
$: width = Math.abs(width);
$: height = Math.abs(height);
if(!centerX){
centerX = left + width / 2;
}
if(!centerY){
centerY = top + height / 2;
}
$: calcAttrs = {
width,
height,
angle: rotateAngle,
left,
top,
centerX,
centerY
};
$: controls = [];
$: if (rotatable) {
controls[0] = {
type: "rotate",
direction: "rot",
x: calcAttrs.left + calcAttrs.width / 2,
y: calcAttrs.top,
};
}
$: {
let i = 1;
controls[i] = {
type: "resize",
direction: "e",
x: calcAttrs.left + calcAttrs.width,
y: calcAttrs.top + calcAttrs.height / 2,
};i++
controls[i] = {
type: "resize",
direction: "ne",
x: calcAttrs.left + calcAttrs.width,
y: calcAttrs.top,
};i++
controls[i] = {
type: "resize",
direction: "se",
x: calcAttrs.left + calcAttrs.width,
y: calcAttrs.top + calcAttrs.height,
};i++
controls[i] = {
type: "resize",
direction: "s",
x: calcAttrs.left + calcAttrs.width / 2,
y: calcAttrs.top + calcAttrs.height,
};i++
controls[i] = {
type: "resize",
direction: "sw",
x: calcAttrs.left,
y: calcAttrs.top + calcAttrs.height,
};i++
controls[i] = {
type: "resize",
direction: "w",
x: calcAttrs.left,
y: calcAttrs.top + calcAttrs.height / 2,
};i++
controls[i] = {
type: "resize",
direction: "nw",
x: calcAttrs.left,
y: calcAttrs.top,
};i++
controls[i] = {
type: "resize",
direction: "n",
x: calcAttrs.left + calcAttrs.width / 2,
y: calcAttrs.top,
};
controls = controls;
}
// Drag
const startDrag = (e) => {
let { clientX: startX, clientY: startY } = e;
const onMove = (e) => {
e.stopImmediatePropagation();
const { clientX, clientY } = e;
const deltaX = clientX - startX;
const deltaY = clientY - startY;
calcAttrs.left = calcAttrs.left + deltaX;
calcAttrs.top = calcAttrs.top + deltaY;
calcAttrs.centerY = calcAttrs.top + calcAttrs.height / 2;
calcAttrs.centerX = calcAttrs.left + calcAttrs.width / 2;
startX = clientX;
startY = clientY;
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
};
// Rotate
const startRotate = (e) => {
if (e.button !== 0) return;
const { clientX, clientY } = e;
const rect = selectBox.getBoundingClientRect();
const center = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
const startVector = {
x: clientX - center.x,
y: clientY - center.y,
};
let startAngle = calcAttrs.angle;
const onMove = (e) => {
e.stopImmediatePropagation();
const { clientX, clientY } = e;
const rotateVector = {
x: clientX - center.x,
y: clientY - center.y,
};
const angle = getAngle(startVector, rotateVector);
calcAttrs.angle = handleRotate(angle, startAngle);
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
};
const handleRotate = (angle, startAngle) => {
let newRotateAngle = Math.round(startAngle + angle)
if (newRotateAngle >= 360) {
newRotateAngle -= 360
} else if (newRotateAngle < 0) {
newRotateAngle += 360
}
if (newRotateAngle > 356 || newRotateAngle < 4) {
newRotateAngle = 0
} else if (newRotateAngle > 86 && newRotateAngle < 94) {
newRotateAngle = 90
} else if (newRotateAngle > 176 && newRotateAngle < 184) {
newRotateAngle = 180
} else if (newRotateAngle > 266 && newRotateAngle < 274) {
newRotateAngle = 270
}
return newRotateAngle
}
var testElement, testControl, start;
// Resize
const startResize = (e) => {
if (e.button !== 0) return;
const { clientX: startX, clientY: startY } = e;
let startTop = calcAttrs.top + calcAttrs.height/2;
let startLeft = calcAttrs.left + calcAttrs.width/2;
start = { width: calcAttrs.width, height: calcAttrs.height, centerX: startLeft, centerY: startTop, rotateAngle: calcAttrs.angle };
const direction = e.target.getAttribute("class").split(" ")[0];
document.body.style.cursor = direction+'-resize';
const onMove = (e) => {
e.stopImmediatePropagation();
const { clientX, clientY } = e;
const deltaX = clientX - startX;
const deltaY = clientY - startY;
const alpha = Math.atan2(deltaY, deltaX);
const deltaL = getLength(deltaX, deltaY);
const beta = alpha - degToRadian(calcAttrs.angle)
const deltaW = deltaL * Math.cos(beta)
const deltaH = deltaL * Math.sin(beta)
let calcedAttrs = getCalcedPosSize(direction, start, deltaW, deltaH, 0, minWidth, minHeight);
calcAttrs.height = calcedAttrs.size.height;
calcAttrs.width = calcedAttrs.size.width;
calcAttrs.top = calcedAttrs.position.centerY - calcedAttrs.size.height / 2;
calcAttrs.left = calcedAttrs.position.centerX - calcedAttrs.size.width / 2;
calcAttrs.centerY = calcAttrs.top + calcAttrs.height / 2;
calcAttrs.centerX = calcAttrs.left + calcAttrs.width / 2;
};
const onUp = () => {
document.body.style.cursor = "auto";
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
};
$: if (!Array.isArray(elements)){
elements = [elements];
}
</script>
<g
transform={`rotate(${calcAttrs.angle} ${calcAttrs.centerX} ${calcAttrs.centerY})`}
>
<g
class="transfer-elements"
>
{#if Array.isArray(elements)}
{#each elements as element}
{#if element.type === 'line'}
<line
x1={element.x1}
y1={element.y1}
x2={element.x2}
y2={element.y2}
stroke="black"
stroke-width={5}
class="control-point"
/>
{/if}
{#if element.type === 'rect'}
<rect
x={element.x}
y={element.y}
width={element.width}
height={element.height}
/>
{/if}
{#if element.type === 'path'}
<path
d={element.d}
/>
{/if}
{/each}
{/if}
</g>
<rect
x={calcAttrs.left}
y={calcAttrs.top}
width={calcAttrs.width}
height={calcAttrs.height}
stroke={color}
stroke-width={strokeWidth}
fill="transparent"
class="bounding-box single-resizer"
on:pointerdown={startDrag}
bind:this={selectBox}
/>
{#each controls as control}
{#if control.type === "rotate"}
<circle
fill="white"
stroke={color}
stroke-width={strokeWidth}
cx={control.x}
cy={control.y - 35}
r={ctrlWidth / 2}
class="rotate control-point"
on:pointerdown={startRotate}
/>
{/if}
{#if control.type === "resize"}
<rect
x={control.x - ctrlWidth / 2}
y={control.y - ctrlWidth / 2}
width={ctrlWidth}
height={ctrlWidth}
fill="white"
stroke={color}
stroke-width={strokeWidth}
class={`${control.direction} resizable-handler control-point`}
on:pointerdown={startResize}
/>
{/if}
{/each}
</g>
<style>
.resizable-handler.n{
cursor: n-resize;
}
.resizable-handler.nw{
cursor: nw-resize;
}
.resizable-handler.w{
cursor: w-resize;
}
.resizable-handler.sw{
cursor: sw-resize;
}
.resizable-handler.s{
cursor: s-resize;
}
.resizable-handler.se{
cursor: se-resize;
}
.resizable-handler.ne{
cursor: ne-resize;
}
.resizable-handler.e{
cursor: e-resize;
}
</style>