How to resize a rotated element using mouse pointer by dragging controls in the selectbox for svg?

628 views Asked by At

I've been trying to make an svg editor but I'm having trouble with resizing line elements. The line won't stay inside the select box and resize along with the select box. In the gif below you can see the issue with line:

Rotating and resizing element issue

I've used this project to implement the select box part in svelte but so far I wasn't able to do that for the actual line element I need to resize. I could use the controls coordinates but thats not ideal for elements like path or I'm not seeing how I could do this. I just couldn't figure out the math behind this used for the select box. I've been doing this for a while so at least some pointers would be appreciated. You can checkout my project in this codesandbox, its in typescript.

This file has helper functions for the svelte select box component. getCalcedPosSize function is where the select boxes rect element attributes are getting their new values.

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
    };
}

This is the select box svelte component.

<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>
1

There are 1 answers

2
Mahmudul Hasan On

Rather than building an svg editor from scratch, you can use fabricjs. http://fabricjs.com