0

我一直在尝试制作一个 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>
4

1 回答 1

-2

您可以使用 fabricjs,而不是从头开始构建 svg 编辑器。 http://fabricjs.com

于 2021-12-03T23:46:24.213 回答